From 4097c4a11d3d9713d7fb085d5969c0cf7ece8c52 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 30 Jan 2024 16:42:36 +0100 Subject: [PATCH 01/68] ci: Use larger runners for playwright tests (#10417) --- .github/workflows/build.yml | 6 +++--- .../tracing/browserTracingIntegration/interactions/test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf3b7d39d360..649f5388ea13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -548,7 +548,7 @@ jobs: name: Playwright (${{ matrix.bundle }}) Tests needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04-large-js timeout-minutes: 25 strategy: fail-fast: false @@ -669,7 +669,7 @@ jobs: name: Browser (${{ matrix.browser }}) Tests needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_browser == 'true' || github.event_name != 'pull_request' - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04-large-js timeout-minutes: 20 strategy: fail-fast: false @@ -839,7 +839,7 @@ jobs: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build] - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04-large-js timeout-minutes: 15 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) 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 131403756251..50c095dbcc57 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 @@ -51,7 +51,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN expect(interactionSpan.timestamp).toBeDefined(); const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000; - expect(interactionSpanDuration).toBeGreaterThan(70); + expect(interactionSpanDuration).toBeGreaterThan(65); expect(interactionSpanDuration).toBeLessThan(200); }); From a37fabdfe464467fa742842bc2242e10e831144b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 30 Jan 2024 17:48:22 +0100 Subject: [PATCH 02/68] build(ci): Bump pr-labels-action to use node20 (#10416) I also opened a PR upstream: https://github.com/joerick/pr-labels-action/pull/17 but merging speed is rather slow there, so I'd rather not wait for this. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 649f5388ea13..bdb0b4d0866f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,7 +136,7 @@ jobs: - name: Get PR labels id: pr-labels - uses: mydea/pr-labels-action@update-core + uses: mydea/pr-labels-action@fn/bump-node20 outputs: commit_label: '${{ env.COMMIT_SHA }}: ${{ env.COMMIT_MESSAGE }}' From 1fe5c0134e2792ebe6f164f46cd27dcc1ed3d020 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 30 Jan 2024 17:49:12 +0100 Subject: [PATCH 03/68] ref(ember): Use new `browserTracingIntegration()` under the hood (#10373) Refactors the usage of `BrowserTracing()` for Ember. There it is easy to refactor this because we do not expose this to the user - we automatically add the browsertracing integration based on configuration. This depends on https://github.com/getsentry/sentry-javascript/pull/10372. --- .../sentry-performance.ts | 131 +++++++++--------- packages/ember/addon/types.ts | 2 +- packages/ember/tests/helpers/setup-sentry.ts | 22 --- packages/ember/tests/helpers/utils.ts | 4 +- 4 files changed, 69 insertions(+), 90 deletions(-) diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index acabe5334cad..c86e280d167b 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -8,18 +8,13 @@ import type { EmberRunQueues } from '@ember/runloop/-private/types'; import { getOwnConfig, isTesting, macroCondition } from '@embroider/macros'; import * as Sentry from '@sentry/browser'; import type { ExtendedBackburner } from '@sentry/ember/runloop'; -import type { Span, Transaction } from '@sentry/types'; +import type { Span } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { BrowserClient } from '..'; import { getActiveSpan, startInactiveSpan } from '..'; -import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types'; - -type SentryTestRouterService = RouterService & { - _startTransaction?: StartTransactionFunction; - _sentryInstrumented?: boolean; -}; +import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; function getSentryConfig(): EmberSentryConfig { const _global = GLOBAL_OBJ as typeof GLOBAL_OBJ & GlobalConfig; @@ -98,26 +93,25 @@ export function _instrumentEmberRouter( routerService: RouterService, routerMain: EmberRouterMain, config: EmberSentryConfig, - startTransaction: StartTransactionFunction, - startTransactionOnPageLoad?: boolean, -): { - startTransaction: StartTransactionFunction; -} { +): void { const { disableRunloopPerformance } = config; const location = routerMain.location; - let activeTransaction: Transaction | undefined; + let activeRootSpan: Span | undefined; let transitionSpan: Span | undefined; + // Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred. + const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {}; const url = getLocationURL(location); - if (macroCondition(isTesting())) { - (routerService as SentryTestRouterService)._sentryInstrumented = true; - (routerService as SentryTestRouterService)._startTransaction = startTransaction; + const client = Sentry.getClient(); + + if (!client) { + return; } - if (startTransactionOnPageLoad && url) { + if (url && browserTracingOptions.startTransactionOnPageLoad !== false) { const routeInfo = routerService.recognize(url); - activeTransaction = startTransaction({ + Sentry.startBrowserTracingPageLoadSpan(client, { name: `route:${routeInfo.name}`, op: 'pageload', origin: 'auto.pageload.ember', @@ -127,20 +121,26 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); + activeRootSpan = getActiveSpan(); } const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => { if (nextInstance) { return; } - activeTransaction?.end(); + activeRootSpan?.end(); getBackburner().off('end', finishActiveTransaction); }; + if (browserTracingOptions.startTransactionOnLocationChange === false) { + return; + } + routerService.on('routeWillChange', (transition: Transition) => { const { fromRoute, toRoute } = getTransitionInformation(transition, routerService); - activeTransaction?.end(); - activeTransaction = startTransaction({ + activeRootSpan?.end(); + + Sentry.startBrowserTracingNavigationSpan(client, { name: `route:${toRoute}`, op: 'navigation', origin: 'auto.navigation.ember', @@ -150,6 +150,9 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); + + activeRootSpan = getActiveSpan(); + transitionSpan = startInactiveSpan({ attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', @@ -160,22 +163,18 @@ export function _instrumentEmberRouter( }); routerService.on('routeDidChange', () => { - if (!transitionSpan || !activeTransaction) { + if (!transitionSpan || !activeRootSpan) { return; } transitionSpan.end(); if (disableRunloopPerformance) { - activeTransaction.end(); + activeRootSpan.end(); return; } getBackburner().on('end', finishActiveTransaction); }); - - return { - startTransaction, - }; } function _instrumentEmberRunloop(config: EmberSentryConfig): void { @@ -411,61 +410,63 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) // Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred. const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {}; - const { BrowserTracing } = await import('@sentry/browser'); + const { browserTracingIntegration } = await import('@sentry/browser'); const idleTimeout = config.transitionTimeout || 5000; - const browserTracing = new BrowserTracing({ - routingInstrumentation: (customStartTransaction, startTransactionOnPageLoad) => { - // eslint-disable-next-line ember/no-private-routing-service - const routerMain = appInstance.lookup('router:main') as EmberRouterMain; - let routerService = appInstance.lookup('service:router') as RouterService & { - externalRouter?: RouterService; - _hasMountedSentryPerformanceRouting?: boolean; - }; - - if (routerService.externalRouter) { - // Using ember-engines-router-service in an engine. - routerService = routerService.externalRouter; - } - if (routerService._hasMountedSentryPerformanceRouting) { - // Routing listens to route changes on the main router, and should not be initialized multiple times per page. - return; - } - if (!routerService.recognize) { - // Router is missing critical functionality to limit cardinality of the transaction names. - return; - } - routerService._hasMountedSentryPerformanceRouting = true; - _instrumentEmberRouter(routerService, routerMain, config, customStartTransaction, startTransactionOnPageLoad); - }, + const browserTracing = browserTracingIntegration({ idleTimeout, ...browserTracingOptions, + instrumentNavigation: false, + instrumentPageLoad: false, }); - if (macroCondition(isTesting())) { - const client = Sentry.getClient(); - - if ( - client && - (client as BrowserClient).getIntegrationByName && - (client as BrowserClient).getIntegrationByName('BrowserTracing') - ) { - // Initializers are called more than once in tests, causing the integrations to not be setup correctly. - return; - } - } + const client = Sentry.getClient(); + + const isAlreadyInitialized = macroCondition(isTesting()) ? !!client?.getIntegrationByName('BrowserTracing') : false; - const client = Sentry.getClient(); if (client && client.addIntegration) { client.addIntegration(browserTracing); } + // We _always_ call this, as it triggers the page load & navigation spans + _instrumentNavigation(appInstance, config); + + // Skip instrumenting the stuff below again in tests, as these are not reset between tests + if (isAlreadyInitialized) { + return; + } + _instrumentEmberRunloop(config); _instrumentComponents(config); _instrumentInitialLoad(config); } +function _instrumentNavigation(appInstance: ApplicationInstance, config: EmberSentryConfig): void { + // eslint-disable-next-line ember/no-private-routing-service + const routerMain = appInstance.lookup('router:main') as EmberRouterMain; + let routerService = appInstance.lookup('service:router') as RouterService & { + externalRouter?: RouterService; + _hasMountedSentryPerformanceRouting?: boolean; + }; + + if (routerService.externalRouter) { + // Using ember-engines-router-service in an engine. + routerService = routerService.externalRouter; + } + if (routerService._hasMountedSentryPerformanceRouting) { + // Routing listens to route changes on the main router, and should not be initialized multiple times per page. + return; + } + if (!routerService.recognize) { + // Router is missing critical functionality to limit cardinality of the transaction names. + return; + } + + routerService._hasMountedSentryPerformanceRouting = true; + _instrumentEmberRouter(routerService, routerMain, config); +} + export default { initialize, }; diff --git a/packages/ember/addon/types.ts b/packages/ember/addon/types.ts index 787eecc7e4cf..868d6265f62d 100644 --- a/packages/ember/addon/types.ts +++ b/packages/ember/addon/types.ts @@ -31,7 +31,7 @@ export interface EmberRouterMain { rootURL: string; }; } - +/** @deprecated This will be removed in v8. */ export type StartTransactionFunction = (context: TransactionContext) => Transaction | undefined; export type GlobalConfig = { diff --git a/packages/ember/tests/helpers/setup-sentry.ts b/packages/ember/tests/helpers/setup-sentry.ts index d8bb513dcd00..db439d226dc2 100644 --- a/packages/ember/tests/helpers/setup-sentry.ts +++ b/packages/ember/tests/helpers/setup-sentry.ts @@ -1,14 +1,7 @@ -import type RouterService from '@ember/routing/router-service'; import type { TestContext } from '@ember/test-helpers'; import { resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { _instrumentEmberRouter } from '@sentry/ember/instance-initializers/sentry-performance'; -import type { EmberRouterMain, EmberSentryConfig, StartTransactionFunction } from '@sentry/ember/types'; import sinon from 'sinon'; -// Keep a reference to the original startTransaction as the application gets re-initialized and setup for -// the integration doesn't occur again after the first time. -let _routerStartTransaction: StartTransactionFunction | undefined; - export type SentryTestContext = TestContext & { errorMessages: string[]; fetchStub: sinon.SinonStub; @@ -16,11 +9,6 @@ export type SentryTestContext = TestContext & { _windowOnError: OnErrorEventHandler; }; -type SentryRouterService = RouterService & { - _startTransaction: StartTransactionFunction; - _sentryInstrumented?: boolean; -}; - export function setupSentryTest(hooks: NestedHooks): void { hooks.beforeEach(async function (this: SentryTestContext) { await window._sentryPerformanceLoad; @@ -28,16 +16,6 @@ export function setupSentryTest(hooks: NestedHooks): void { const errorMessages: string[] = []; this.errorMessages = errorMessages; - // eslint-disable-next-line ember/no-private-routing-service - const routerMain = this.owner.lookup('router:main') as EmberRouterMain; - const routerService = this.owner.lookup('service:router') as SentryRouterService; - - if (routerService._sentryInstrumented) { - _routerStartTransaction = routerService._startTransaction; - } else if (_routerStartTransaction) { - _instrumentEmberRouter(routerService, routerMain, {} as EmberSentryConfig, _routerStartTransaction); - } - /** * Stub out fetch function to assert on Sentry calls. */ diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index a14088a2329e..16a34bde340c 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -58,8 +58,8 @@ export function assertSentryTransactions( const sentryTestEvents = getTestSentryTransactions(); const event = sentryTestEvents[callNumber]; - assert.ok(event); - assert.ok(event.spans); + assert.ok(event, 'event exists'); + assert.ok(event.spans, 'event has spans'); const spans = event.spans || []; From 5d10cdbc03970fdb34023229b19f0822f9f5938e Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 30 Jan 2024 13:07:39 -0500 Subject: [PATCH 04/68] ref(vue): use startInactiveSpan in tracing mixin (#10406) ref https://github.com/getsentry/sentry-javascript/issues/10100 Cannot get rid of transactions completely because we rely on accessing the root span to mutate. --- packages/vue/src/tracing.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index a5ade00e8ec0..8e2c743db064 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '@sentry/browser'; +import { getActiveSpan, getCurrentScope, startInactiveSpan } from '@sentry/browser'; import type { Span, Transaction } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; @@ -78,14 +78,12 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { const isRoot = this.$root === this; if (isRoot) { - // eslint-disable-next-line deprecation/deprecation - const activeTransaction = getActiveTransaction(); - if (activeTransaction) { + const activeSpan = getActiveSpan(); + if (activeSpan) { this.$_sentryRootSpan = this.$_sentryRootSpan || - // eslint-disable-next-line deprecation/deprecation - activeTransaction.startChild({ - description: 'Application Render', + startInactiveSpan({ + name: 'Application Render', op: `${VUE_OP}.render`, origin: 'auto.ui.vue', }); @@ -108,9 +106,8 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // Start a new span if current hook is a 'before' hook. // Otherwise, retrieve the current span and finish it. if (internalHook == internalHooks[0]) { - // eslint-disable-next-line deprecation/deprecation - const activeTransaction = (this.$root && this.$root.$_sentryRootSpan) || getActiveTransaction(); - if (activeTransaction) { + const activeSpan = (this.$root && this.$root.$_sentryRootSpan) || getActiveSpan(); + if (activeSpan) { // Cancel old span for this hook operation in case it didn't get cleaned up. We're not actually sure if it // will ever be the case that cleanup hooks re not called, but we had users report that spans didn't get // finished so we finish the span before starting a new one, just to be sure. @@ -119,9 +116,8 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { oldSpan.end(); } - // eslint-disable-next-line deprecation/deprecation - this.$_sentrySpans[operation] = activeTransaction.startChild({ - description: `Vue <${name}>`, + this.$_sentrySpans[operation] = startInactiveSpan({ + name: `Vue <${name}>`, op: `${VUE_OP}.${operation}`, origin: 'auto.ui.vue', }); From e16869235f696807f91b484b7283ede00f19fb8b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 Jan 2024 09:28:50 +0100 Subject: [PATCH 05/68] test(e2e): Add Angular 17 E2E test (#10427) This PR adds an e2e test app for Angular with 3 tests: 1. SDK sends error 2. SDK sends parameterized pageload transaction 3. SDK sends parameterized navigation transaction --- .github/workflows/build.yml | 1 + biome.json | 17 +- .../angular-17/.editorconfig | 16 ++ .../test-applications/angular-17/.gitignore | 42 +++ .../test-applications/angular-17/.npmrc | 2 + .../test-applications/angular-17/README.md | 3 + .../test-applications/angular-17/angular.json | 95 +++++++ .../angular-17/event-proxy-server.ts | 253 ++++++++++++++++++ .../test-applications/angular-17/package.json | 50 ++++ .../angular-17/playwright.config.ts | 77 ++++++ .../angular-17/src/app/app.component.ts | 12 + .../angular-17/src/app/app.config.ts | 25 ++ .../angular-17/src/app/app.routes.ts | 18 ++ .../angular-17/src/app/home/home.component.ts | 22 ++ .../angular-17/src/app/user/user.component.ts | 20 ++ .../angular-17/src/favicon.ico | Bin 0 -> 15086 bytes .../angular-17/src/index.html | 13 + .../test-applications/angular-17/src/main.ts | 19 ++ .../angular-17/src/styles.css | 1 + .../angular-17/start-event-proxy.ts | 6 + .../angular-17/tests/errors.test.ts | 29 ++ .../angular-17/tests/performance.test.ts | 54 ++++ .../angular-17/tsconfig.app.json | 14 + .../angular-17/tsconfig.json | 38 +++ 24 files changed, 825 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/.editorconfig create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/README.md create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/angular.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/package.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/app/app.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/app/app.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/index.html create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/src/styles.css create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/angular-17/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bdb0b4d0866f..472a1ebcba53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -893,6 +893,7 @@ jobs: matrix: test-application: [ + 'angular-17', 'cloudflare-astro', 'node-express-app', 'create-react-app', diff --git a/biome.json b/biome.json index ff5a6ac17286..3fc89f8a7cd3 100644 --- a/biome.json +++ b/biome.json @@ -31,7 +31,18 @@ "noDelete": "off" } }, - "ignore": [".vscode/*", "**/*.json", ".next/**/*", ".svelte-kit/**/*"] + "ignore": [ + ".vscode/*", + "**/*.json", + ".next/**/*", + ".svelte-kit/**/*", + "**/fixtures/*/*.json", + "**/*.min.js", + ".next/**", + ".svelte-kit/**", + ".angular/**", + "angular.json" + ] }, "files": { "ignoreUnknown": true @@ -51,7 +62,9 @@ "**/fixtures/*/*.json", "**/*.min.js", ".next/**", - ".svelte-kit/**" + ".svelte-kit/**", + ".angular/**", + "angular.json" ] }, "javascript": { diff --git a/dev-packages/e2e-tests/test-applications/angular-17/.editorconfig b/dev-packages/e2e-tests/test-applications/angular-17/.editorconfig new file mode 100644 index 000000000000..59d9a3a3e73f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/dev-packages/e2e-tests/test-applications/angular-17/.gitignore b/dev-packages/e2e-tests/test-applications/angular-17/.gitignore new file mode 100644 index 000000000000..0711527ef9d5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/dev-packages/e2e-tests/test-applications/angular-17/.npmrc b/dev-packages/e2e-tests/test-applications/angular-17/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/.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/angular-17/README.md b/dev-packages/e2e-tests/test-applications/angular-17/README.md new file mode 100644 index 000000000000..0b2e08b54d34 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/README.md @@ -0,0 +1,3 @@ +# Angular17 + +E2E test app for Angular 17 and `@sentry/angular-ivy`. diff --git a/dev-packages/e2e-tests/test-applications/angular-17/angular.json b/dev-packages/e2e-tests/test-applications/angular-17/angular.json new file mode 100644 index 000000000000..387a7eefce16 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/angular.json @@ -0,0 +1,95 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-17": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/angular-17", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-17:build:production" + }, + "development": { + "buildTarget": "angular-17:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "angular-17:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts new file mode 100644 index 000000000000..4c2df32399f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/event-proxy-server.ts @@ -0,0 +1,253 @@ +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, SerializedEvent } 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, new TextEncoder(), new TextDecoder()), + 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: SerializedEvent) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: SerializedEvent) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); + 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/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json new file mode 100644 index 000000000000..c81a2d19c8c4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -0,0 +1,50 @@ +{ + "name": "angular-17", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "dev": "ng serve", + "preview": "http-server dist/angular-17/browser", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "playwright test", + "clean": "npx rimraf .angular,node_modules,pnpm-lock.yaml,dist" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.1.0", + "@angular/common": "^17.1.0", + "@angular/compiler": "^17.1.0", + "@angular/core": "^17.1.0", + "@angular/forms": "^17.1.0", + "@angular/platform-browser": "^17.1.0", + "@angular/platform-browser-dynamic": "^17.1.0", + "@angular/router": "^17.1.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.3" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.1.1", + "@angular/cli": "^17.1.1", + "@angular/compiler-cli": "^17.1.0", + "@playwright/test": "^1.41.1", + "@sentry/angular-ivy": "latest || *", + "@types/jasmine": "~5.1.0", + "http-server": "^14.1.1", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.3.2", + "ts-node": "10.9.1", + "wait-port": "1.0.4" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/playwright.config.ts b/dev-packages/e2e-tests/test-applications/angular-17/playwright.config.ts new file mode 100644 index 000000000000..967aad98df5e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/playwright.config.ts @@ -0,0 +1,77 @@ +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 testEnv = process.env['TEST_ENV'] || 'production'; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const angularPort = 8080; +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: 10000, + }, + /* 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, + /* `next dev` is incredibly buggy with the app dir */ + retries: testEnv === 'development' ? 3 : 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:${angularPort}`, + + /* 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'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: + testEnv === 'development' + ? `pnpm wait-port ${eventProxyPort} && pnpm preview -p ${angularPort}` + : `pnpm wait-port ${eventProxyPort} && pnpm preview -p ${angularPort}`, + port: angularPort, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.component.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.component.ts new file mode 100644 index 000000000000..989003bef670 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, +}) +export class AppComponent { + title = 'angular-17'; +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.config.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.config.ts new file mode 100644 index 000000000000..44cf67e5875d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.config.ts @@ -0,0 +1,25 @@ +import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'; +import { Router, provideRouter } from '@angular/router'; + +import { TraceService, createErrorHandler } from '@sentry/angular-ivy'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes), + { + provide: ErrorHandler, + useValue: createErrorHandler(), + }, + { + provide: TraceService, + deps: [Router], + }, + { + provide: APP_INITIALIZER, + useFactory: () => () => {}, + deps: [TraceService], + multi: true, + }, + ], +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts new file mode 100644 index 000000000000..0b44bc341a9b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/app.routes.ts @@ -0,0 +1,18 @@ +import { Routes } from '@angular/router'; +import { HomeComponent } from './home/home.component'; +import { UserComponent } from './user/user.component'; + +export const routes: Routes = [ + { + path: 'users/:id', + component: UserComponent, + }, + { + path: 'home', + component: HomeComponent, + }, + { + path: '**', + redirectTo: 'home', + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts new file mode 100644 index 000000000000..58a375be1a2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/home/home.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [RouterLink], + template: ` +
+

Welcome to Sentry's Angular 17 E2E test app

+ + +
+`, +}) +export class HomeComponent { + throwError() { + throw new Error('Error thrown from Angular 17 E2E test app'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts new file mode 100644 index 000000000000..087a33a4a2f1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/user/user.component.ts @@ -0,0 +1,20 @@ +import { AsyncPipe } from '@angular/common'; +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, map } from 'rxjs'; + +@Component({ + selector: 'app-user', + standalone: true, + imports: [AsyncPipe], + template: ` +

Hello User {{ userId$ | async }}

+ `, +}) +export class UserComponent { + public userId$: Observable; + + constructor(private route: ActivatedRoute) { + this.userId$ = this.route.paramMap.pipe(map(params => params.get('id') || 'UNKNOWN USER')); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/favicon.ico b/dev-packages/e2e-tests/test-applications/angular-17/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/index.html b/dev-packages/e2e-tests/test-applications/angular-17/src/index.html new file mode 100644 index 000000000000..d7d32515339e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/index.html @@ -0,0 +1,13 @@ + + + + + Angular17 + + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts new file mode 100644 index 000000000000..7732c602bb28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts @@ -0,0 +1,19 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +import * as Sentry from '@sentry/angular-ivy'; + +Sentry.init({ + dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + tracesSampleRate: 1.0, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.routingInstrumentation, + }), + ], + tunnel: `http://localhost:3031/`, // proxy server + debug: true, +}); + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/styles.css b/dev-packages/e2e-tests/test-applications/angular-17/src/styles.css new file mode 100644 index 000000000000..90d4ee0072ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ 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 new file mode 100644 index 000000000000..56fe43416adc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'angular-17', +}); 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 new file mode 100644 index 000000000000..d34f6a83eb29 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('angular-17', async errorEvent => { + return !errorEvent?.transaction; + }); + + await page.goto(`/`); + + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Angular 17 E2E test app', + mechanism: { + type: 'angular', + handled: false, + }, + }, + ], + }, + }); +}); 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 new file mode 100644 index 000000000000..8cda20ec3853 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json new file mode 100644 index 000000000000..374cc9d294aa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.json b/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.json new file mode 100644 index 000000000000..e850ebdafb6e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.json @@ -0,0 +1,38 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } +} From 34f9cc701d72a77d732f851bac6a1c0bd1788f60 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 10:12:41 +0100 Subject: [PATCH 06/68] test: Make some replay tests less flaky (#10418) This changes the replay fetch/xhr tests to not wait for the initial replay, but wait for any replay being captured, and collect snapshots as we go. This way, we may be a bit more resilient for small timing issues - hopefully... --- .github/workflows/build.yml | 2 +- .github/workflows/flaky-test-detector.yml | 2 +- .../fetch/captureRequestBody/test.ts | 49 ++++++++++--------- .../fetch/captureRequestHeaders/test.ts | 49 ++++++++++--------- .../fetch/captureRequestSize/test.ts | 22 +++++---- .../fetch/captureResponseBody/test.ts | 40 ++++++++------- .../fetch/captureResponseHeaders/test.ts | 31 ++++++------ .../fetch/captureResponseSize/test.ts | 34 +++++++------ .../fetch/captureTimestamps/test.ts | 13 ++--- .../xhr/captureRequestBody/test.ts | 49 ++++++++++--------- .../xhr/captureRequestHeaders/test.ts | 24 +++++---- .../xhr/captureRequestSize/test.ts | 22 +++++---- .../xhr/captureResponseBody/test.ts | 49 ++++++++++--------- .../xhr/captureResponseHeaders/test.ts | 22 +++++---- .../xhr/captureResponseSize/test.ts | 31 ++++++------ .../xhr/captureTimestamps/test.ts | 14 +++--- .../utils/replayHelpers.ts | 47 +++++++++++++++++- 17 files changed, 295 insertions(+), 205 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 472a1ebcba53..9153498ecc97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -381,7 +381,7 @@ jobs: name: Browser Unit Tests needs: [job_get_metadata, job_build] timeout-minutes: 10 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 1207c9fbf3fd..7774ca1d8d0b 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -23,7 +23,7 @@ concurrency: jobs: flaky-detector: - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04-large-js timeout-minutes: 60 name: 'Check tests for flakiness' # Also skip if PR is from master -> develop diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts index 4f29b0422d2a..bd8050b740aa 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures text request body', async ({ getLocalTestPath, page, browserName }) => { @@ -30,7 +30,9 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -65,9 +67,8 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -109,7 +110,9 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -144,9 +147,8 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -188,7 +190,9 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -227,9 +231,8 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -271,7 +274,9 @@ sentryTest('captures text request body when matching relative URL', async ({ get }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); @@ -306,9 +311,8 @@ sentryTest('captures text request body when matching relative URL', async ({ get }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -348,7 +352,9 @@ sentryTest('does not capture request body when URL does not match', async ({ get }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -383,9 +389,8 @@ sentryTest('does not capture request body when URL does not match', async ({ get }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts index d1cc0a58e118..68296df30cdd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('handles empty/missing request headers', async ({ getLocalTestPath, page, browserName }) => { @@ -28,7 +28,9 @@ sentryTest('handles empty/missing request headers', async ({ getLocalTestPath, p }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -61,9 +63,8 @@ sentryTest('handles empty/missing request headers', async ({ getLocalTestPath, p }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -100,7 +101,9 @@ sentryTest('captures request headers as POJO', async ({ getLocalTestPath, page, }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -140,9 +143,8 @@ sentryTest('captures request headers as POJO', async ({ getLocalTestPath, page, }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -184,7 +186,9 @@ sentryTest('captures request headers on Request', async ({ getLocalTestPath, pag }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -224,9 +228,8 @@ sentryTest('captures request headers on Request', async ({ getLocalTestPath, pag }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -267,7 +270,9 @@ sentryTest('captures request headers as Headers instance', async ({ getLocalTest }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); @@ -308,9 +313,8 @@ sentryTest('captures request headers as Headers instance', async ({ getLocalTest }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -351,7 +355,9 @@ sentryTest('does not captures request headers if URL does not match', async ({ g }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -391,9 +397,8 @@ sentryTest('does not captures request headers if URL does not match', async ({ g }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts index 3e250bd20df3..bc79df066246 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures request body size when body is sent', async ({ getLocalTestPath, page }) => { @@ -28,7 +28,9 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -63,9 +65,8 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -112,7 +113,9 @@ sentryTest('captures request size from non-text request body', async ({ getLocal }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -149,9 +152,8 @@ sentryTest('captures request size from non-text request body', async ({ getLocal }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts index b1c0a496476e..c4607fa9cbf7 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures text response body', async ({ getLocalTestPath, page, browserName }) => { @@ -31,7 +31,9 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -65,9 +67,8 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -112,7 +113,9 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -146,9 +149,8 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -193,7 +195,9 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -227,9 +231,8 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', @@ -272,7 +275,9 @@ sentryTest('does not capture response body when URL does not match', async ({ ge }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -306,9 +311,8 @@ sentryTest('does not capture response body when URL does not match', async ({ ge }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts index 93fe566c6bb6..c587db401e4f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('handles empty headers', async ({ getLocalTestPath, page, browserName }) => { @@ -30,7 +30,9 @@ sentryTest('handles empty headers', async ({ getLocalTestPath, page, browserName }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -61,9 +63,8 @@ sentryTest('handles empty headers', async ({ getLocalTestPath, page, browserName }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'GET', @@ -105,7 +106,9 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page }) => { }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -136,9 +139,8 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page }) => { }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'GET', @@ -186,7 +188,9 @@ sentryTest('does not capture response headers if URL does not match', async ({ g }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -217,9 +221,8 @@ sentryTest('does not capture response headers if URL does not match', async ({ g }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'GET', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts index cba36c1814b9..ad3aafe34562 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures response size from Content-Length header if available', async ({ getLocalTestPath, page }) => { @@ -35,7 +35,10 @@ sentryTest('captures response size from Content-Length header if available', asy }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -67,9 +70,8 @@ sentryTest('captures response size from Content-Length header if available', asy }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'GET', @@ -123,7 +125,10 @@ sentryTest('captures response size without Content-Length header', async ({ getL }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -155,9 +160,8 @@ sentryTest('captures response size without Content-Length header', async ({ getL }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'GET', @@ -208,7 +212,10 @@ sentryTest('captures response size from non-text response body', async ({ getLoc }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -241,9 +248,8 @@ sentryTest('captures response size from non-text response body', async ({ getLoc }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts index 203a89caaaab..cce931062770 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, browserName }) => { @@ -30,7 +30,9 @@ sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, brows }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -50,10 +52,9 @@ sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, brows const request = await requestPromise; const eventData = envelopeRequestParser(request); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); + const { replayRecordingSnapshots } = await replayRequestPromise; - const xhrSpan = performanceSpans1.find(span => span.op === 'resource.fetch')!; + const xhrSpan = getReplayPerformanceSpans(replayRecordingSnapshots).find(span => span.op === 'resource.fetch')!; expect(xhrSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts index b2d4fddaad9e..cd19ba50dd99 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures text request body', async ({ getLocalTestPath, page, browserName }) => { @@ -29,7 +29,9 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -68,9 +70,8 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -110,7 +111,9 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -149,9 +152,8 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -191,7 +193,9 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -234,9 +238,8 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -276,7 +279,9 @@ sentryTest('captures text request body when matching relative URL', async ({ get }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); @@ -315,9 +320,8 @@ sentryTest('captures text request body when matching relative URL', async ({ get }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -357,7 +361,9 @@ sentryTest('does not capture request body when URL does not match', async ({ get }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -396,9 +402,8 @@ sentryTest('does not capture request body when URL does not match', async ({ get }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts index 7158f034a2ef..c9dd8c455b41 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures request headers', async ({ getLocalTestPath, page, browserName }) => { @@ -29,7 +29,9 @@ sentryTest('captures request headers', async ({ getLocalTestPath, page, browserN }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -54,7 +56,7 @@ sentryTest('captures request headers', async ({ getLocalTestPath, page, browserN /* eslint-enable */ }); - const [request, replayReq1] = await Promise.all([requestPromise, replayRequestPromise1]); + const request = await requestPromise; const eventData = envelopeRequestParser(request); expect(eventData.exception?.values).toHaveLength(1); @@ -71,8 +73,8 @@ sentryTest('captures request headers', async ({ getLocalTestPath, page, browserN }, }); - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -116,7 +118,9 @@ sentryTest( }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -141,7 +145,7 @@ sentryTest( /* eslint-enable */ }); - const [request, replayReq1] = await Promise.all([requestPromise, replayRequestPromise1]); + const [request] = await Promise.all([requestPromise]); const eventData = envelopeRequestParser(request); @@ -159,8 +163,8 @@ sentryTest( }, }); - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts index 15e5cc431d35..d33d8a64f1c1 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures request body size when body is sent', async ({ getLocalTestPath, page, browserName }) => { @@ -29,7 +29,9 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -68,9 +70,8 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -118,7 +119,9 @@ sentryTest('captures request size from non-text request body', async ({ getLocal }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -159,9 +162,8 @@ sentryTest('captures request size from non-text request body', async ({ getLocal }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts index 12ef0b2a6068..97e9bcd749fa 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures text response body', async ({ getLocalTestPath, page, browserName }) => { @@ -33,7 +33,9 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -72,9 +74,8 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -118,7 +119,9 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -157,9 +160,8 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -203,7 +205,9 @@ sentryTest('captures JSON response body when responseType=json', async ({ getLoc }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -244,9 +248,8 @@ sentryTest('captures JSON response body when responseType=json', async ({ getLoc }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -290,7 +293,9 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -329,9 +334,8 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', @@ -377,7 +381,9 @@ sentryTest( }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -416,9 +422,8 @@ sentryTest( }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts index ed2c2f5b2765..754c2adf588f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures response headers', async ({ getLocalTestPath, page, browserName }) => { @@ -36,7 +36,9 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page, browser }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -74,9 +76,8 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page, browser }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'GET', @@ -127,7 +128,9 @@ sentryTest( }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -165,9 +168,8 @@ sentryTest( }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'GET', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts index ea0d6240c8e9..5024e65741e9 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest( @@ -34,7 +34,9 @@ sentryTest( }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -73,9 +75,8 @@ sentryTest( }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'GET', @@ -130,7 +131,9 @@ sentryTest('captures response size without Content-Length header', async ({ getL }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -169,9 +172,8 @@ sentryTest('captures response size without Content-Length header', async ({ getL }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'GET', @@ -223,7 +225,9 @@ sentryTest('captures response size for non-string bodies', async ({ getLocalTest }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -262,9 +266,8 @@ sentryTest('captures response size for non-string bodies', async ({ getLocalTest }, }); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ + const { replayRecordingSnapshots } = await replayRequestPromise; + expect(getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr')).toEqual([ { data: { method: 'POST', diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts index 1a60ceea6509..d5d065f83ec5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts @@ -3,9 +3,9 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers'; import { - getCustomRecordingEvents, + collectReplayRequests, + getReplayPerformanceSpans, shouldSkipReplayTest, - waitForReplayRequest, } from '../../../../../utils/replayHelpers'; sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, browserName }) => { @@ -30,7 +30,9 @@ sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, brows }); const requestPromise = waitForErrorRequest(page); - const replayRequestPromise1 = waitForReplayRequest(page, 0); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -58,10 +60,8 @@ sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, brows const request = await requestPromise; const eventData = envelopeRequestParser(request); - const replayReq1 = await replayRequestPromise1; - const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); - - const xhrSpan = performanceSpans1.find(span => span.op === 'resource.xhr')!; + const { replayRecordingSnapshots } = await replayRequestPromise; + const xhrSpan = getReplayPerformanceSpans(replayRecordingSnapshots).find(span => span.op === 'resource.xhr')!; expect(xhrSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/utils/replayHelpers.ts b/dev-packages/browser-integration-tests/utils/replayHelpers.ts index 87283e2ceb75..f0015d2dfb7f 100644 --- a/dev-packages/browser-integration-tests/utils/replayHelpers.ts +++ b/dev-packages/browser-integration-tests/utils/replayHelpers.ts @@ -104,6 +104,49 @@ export function waitForReplayRequest( ); } +/** + * Collect replay requests until a given callback is satisfied. + * This can be used to ensure we wait correctly, + * when we don't know in which request a certain replay event/snapshot will be. + */ +export function collectReplayRequests( + page: Page, + callback: (replayRecordingEvents: RecordingSnapshot[], replayEvents: ReplayEvent[]) => boolean, +): Promise<{ replayEvents: ReplayEvent[]; replayRecordingSnapshots: RecordingSnapshot[] }> { + const replayEvents: ReplayEvent[] = []; + const replayRecordingSnapshots: RecordingSnapshot[] = []; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + const promise = page.waitForResponse(res => { + const req = res.request(); + + const event = getReplayEventFromRequest(req); + + if (!event) { + return false; + } + + replayEvents.push(event); + replayRecordingSnapshots.push(...getDecompressedRecordingEvents(req)); + + try { + return callback(replayRecordingSnapshots, replayEvents); + } catch { + return false; + } + }); + + const replayRequestPromise = async (): Promise<{ + replayEvents: ReplayEvent[]; + replayRecordingSnapshots: RecordingSnapshot[]; + }> => { + await promise; + return { replayEvents, replayRecordingSnapshots }; + }; + + return replayRequestPromise(); +} + /** * Wait until a callback returns true, collecting all replay responses along the way. * This can be useful when you don't know if stuff will be in one or multiple replay requests. @@ -246,14 +289,14 @@ function getAllCustomRrwebRecordingEvents(recordingEvents: RecordingEvent[]): Cu return recordingEvents.filter(isCustomSnapshot).map(event => event.data); } -function getReplayBreadcrumbs(recordingEvents: RecordingSnapshot[], category?: string): Breadcrumb[] { +export function getReplayBreadcrumbs(recordingEvents: RecordingSnapshot[], category?: string): Breadcrumb[] { return getAllCustomRrwebRecordingEvents(recordingEvents) .filter(data => data.tag === 'breadcrumb') .map(data => data.payload) .filter(payload => !category || payload.category === category); } -function getReplayPerformanceSpans(recordingEvents: RecordingEvent[]): PerformanceSpan[] { +export function getReplayPerformanceSpans(recordingEvents: RecordingSnapshot[]): PerformanceSpan[] { return getAllCustomRrwebRecordingEvents(recordingEvents) .filter(data => data.tag === 'performanceSpan') .map(data => data.payload) as PerformanceSpan[]; From 1ec15ee416f8a371b231bfdcd3f44d2a669e8f96 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 10:15:06 +0100 Subject: [PATCH 07/68] ci: Always run code formatting CI job (even for *.md only PRs) (#10421) This runs a separate, streamlined lint step for prettier only, if only markdown files are changed. --- .github/workflows/build.yml | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9153498ecc97..eda494c77698 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,8 +165,7 @@ jobs: runs-on: ubuntu-20.04 timeout-minutes: 15 if: | - (needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false') && - (needs.job_get_metadata.outputs.changed_any_code == 'true' || github.event_name != 'pull_request') + (needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false') steps: - name: 'Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})' uses: actions/checkout@v4 @@ -215,6 +214,8 @@ jobs: needs: [job_get_metadata, job_install_deps] runs-on: ubuntu-20.04 timeout-minutes: 30 + if: | + (needs.job_get_metadata.outputs.changed_any_code == 'true' || github.event_name != 'pull_request') steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -319,10 +320,33 @@ jobs: env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Lint source files - run: yarn lint + run: yarn lint:lerna - name: Validate ES5 builds run: yarn validate:es5 + job_check_format: + name: Check file formatting + needs: [job_get_metadata, job_install_deps] + timeout-minutes: 10 + runs-on: ubuntu-20.04 + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + - name: Check dependency cache + uses: actions/cache/restore@v3 + with: + path: ${{ env.CACHED_DEPENDENCY_PATHS }} + key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + fail-on-cache-miss: true + - name: Check file formatting + run: yarn lint:prettier && yarn lint:biome + job_circular_dep_check: name: Circular Dependency Check needs: [job_get_metadata, job_build] @@ -1015,6 +1039,7 @@ jobs: job_e2e_tests, job_artifacts, job_lint, + job_check_format, job_circular_dep_check, ] # Always run this, even if a dependent job failed From 6b86b3e35dbc19743722f38cdf9fc1d7fd148dae Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 31 Jan 2024 10:39:18 +0100 Subject: [PATCH 08/68] feat(utils): Add `propagationContextFromHeaders` (#10313) --- packages/core/src/tracing/trace.ts | 19 ++++-- .../pagesRouterRoutingInstrumentation.ts | 1 + .../nextjs/src/common/utils/wrapperUtils.ts | 1 + .../common/withServerActionInstrumentation.ts | 1 + .../src/common/wrapRouteHandlerWithSentry.ts | 1 + packages/node-experimental/src/sdk/init.ts | 4 +- packages/node/src/sdk.ts | 4 +- packages/opentelemetry/src/propagator.ts | 4 +- packages/remix/src/utils/instrumentServer.ts | 1 + packages/sveltekit/src/server/utils.ts | 2 + .../src/browser/browserTracingIntegration.ts | 12 ++-- .../src/browser/browsertracing.ts | 12 ++-- packages/utils/src/tracing.ts | 31 ++++++++++ packages/utils/test/tracing.test.ts | 59 ++++++++++++++++++- 14 files changed, 131 insertions(+), 21 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index dc822a2bab7d..d8109d6a9179 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,5 +1,6 @@ import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; +import type { propagationContextFromHeaders } from '@sentry/utils'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -228,16 +229,16 @@ export function continueTrace({ sentryTrace, baggage, }: { - sentryTrace: Parameters[0]; - baggage: Parameters[1]; + sentryTrace: Parameters[0]; + baggage: Parameters[1]; }): Partial; export function continueTrace( { sentryTrace, baggage, }: { - sentryTrace: Parameters[0]; - baggage: Parameters[1]; + sentryTrace: Parameters[0]; + baggage: Parameters[1]; }, callback: (transactionContext: Partial) => V, ): V; @@ -253,13 +254,23 @@ export function continueTrace( sentryTrace, baggage, }: { + // eslint-disable-next-line deprecation/deprecation sentryTrace: Parameters[0]; + // eslint-disable-next-line deprecation/deprecation baggage: Parameters[1]; }, callback?: (transactionContext: Partial) => V, ): V | Partial { + // TODO(v8): Change this function so it doesn't do anything besides setting the propagation context on the current scope: + /* + const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); + getCurrentScope().setPropagationContext(propagationContext); + return; + */ + const currentScope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTrace, baggage, diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 5f2064c690e4..e360e51df56b 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -119,6 +119,7 @@ export function pagesRouterInstrumentation( startTransactionOnLocationChange: boolean = true, ): void { const { route, params, sentryTrace, baggage } = extractNextDataTagInformation(); + // eslint-disable-next-line deprecation/deprecation const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTrace, baggage, diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index f7e0917f2c39..2b9d58d41616 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -93,6 +93,7 @@ export function withTracedServerSideDataFetcher Pr const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; const baggage = req.headers?.baggage; + // eslint-disable-next-line deprecation/deprecation const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTrace, baggage, diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 01e1c75d6f3f..0d0e6968a3b1 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -76,6 +76,7 @@ async function withServerActionInstrumentationImplementation any>( return new Proxy(routeHandler, { apply: (originalFunction, thisArg, args) => { return runWithAsyncContext(async () => { + // eslint-disable-next-line deprecation/deprecation const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTraceHeader ?? headers?.get('sentry-trace') ?? undefined, baggageHeader ?? headers?.get('baggage'), diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 353d38be90f2..c0ce8056e336 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -12,8 +12,8 @@ import { consoleSandbox, dropUndefinedKeys, logger, + propagationContextFromHeaders, stackParserFromStackParserOptions, - tracingContextFromHeaders, } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -190,7 +190,7 @@ function updateScopeFromEnvVariables(): void { if (!['false', 'n', 'no', 'off', '0'].includes(sentryUseEnvironment)) { const sentryTraceEnv = process.env.SENTRY_TRACE; const baggageEnv = process.env.SENTRY_BAGGAGE; - const { propagationContext } = tracingContextFromHeaders(sentryTraceEnv, baggageEnv); + const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); getCurrentScope().setPropagationContext(propagationContext); } } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 9ef08c88aeb9..8584f66dc083 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -17,8 +17,8 @@ import { GLOBAL_OBJ, createStackParser, nodeStackLineParser, + propagationContextFromHeaders, stackParserFromStackParserOptions, - tracingContextFromHeaders, } from '@sentry/utils'; import { setNodeAsyncContextStrategy } from './async'; @@ -291,7 +291,7 @@ function updateScopeFromEnvVariables(): void { if (!['false', 'n', 'no', 'off', '0'].includes(sentryUseEnvironment)) { const sentryTraceEnv = process.env.SENTRY_TRACE; const baggageEnv = process.env.SENTRY_BAGGAGE; - const { propagationContext } = tracingContextFromHeaders(sentryTraceEnv, baggageEnv); + const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); getCurrentScope().setPropagationContext(propagationContext); } } diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index bbeb2744e501..0f53876f7240 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -3,7 +3,7 @@ import { TraceFlags, propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; import { getDynamicSamplingContextFromClient } from '@sentry/core'; import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; -import { SENTRY_BAGGAGE_KEY_PREFIX, generateSentryTraceHeader, tracingContextFromHeaders } from '@sentry/utils'; +import { SENTRY_BAGGAGE_KEY_PREFIX, generateSentryTraceHeader, propagationContextFromHeaders } from '@sentry/utils'; import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants'; import { getClient } from './custom/hub'; @@ -55,7 +55,7 @@ export class SentryPropagator extends W3CBaggagePropagator { : maybeSentryTraceHeader : undefined; - const { propagationContext } = tracingContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); + const propagationContext = propagationContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); // Add propagation context to context const contextWithPropagationContext = setPropagationContextOnContext(context, propagationContext); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 1ed13e9f28ec..94e8090ac433 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -401,6 +401,7 @@ export function startRequestHandlerTransaction( method: string; }, ): Transaction { + // eslint-disable-next-line deprecation/deprecation const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( request.headers['sentry-trace'], request.headers.baggage, diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 4106f7f4a09c..1f3719745bca 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -10,9 +10,11 @@ import { DEBUG_BUILD } from '../common/debug-build'; * * Sets propagation context as a side effect. */ +// eslint-disable-next-line deprecation/deprecation export function getTracePropagationData(event: RequestEvent): ReturnType { const sentryTraceHeader = event.request.headers.get('sentry-trace') || ''; const baggageHeader = event.request.headers.get('baggage'); + // eslint-disable-next-line deprecation/deprecation return tracingContextFromHeaders(sentryTraceHeader, baggageHeader); } diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 34fe4a7b13d2..aaf30e7e6a63 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -23,7 +23,7 @@ import { browserPerformanceTimeOrigin, getDomElement, logger, - tracingContextFromHeaders, + propagationContextFromHeaders, } from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -203,21 +203,23 @@ export const browserTracingIntegration = ((_options: Partial[0], baggage: Parameters[0], @@ -83,6 +86,34 @@ export function tracingContextFromHeaders( } } +/** + * Create a propagation context from incoming headers. + */ +export function propagationContextFromHeaders( + sentryTrace: string | undefined, + baggage: string | number | boolean | string[] | null | undefined, +): PropagationContext { + const traceparentData = extractTraceparentData(sentryTrace); + const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggage); + + const { traceId, parentSpanId, parentSampled } = traceparentData || {}; + + if (!traceparentData) { + return { + traceId: traceId || uuid4(), + spanId: uuid4().substring(16), + }; + } else { + return { + traceId: traceId || uuid4(), + parentSpanId: parentSpanId || uuid4().substring(16), + spanId: uuid4().substring(16), + sampled: parentSampled, + dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + }; + } +} + /** * Create sentry-trace header from span context values. */ diff --git a/packages/utils/test/tracing.test.ts b/packages/utils/test/tracing.test.ts index 2e7cc4d3d5a5..ee3790322460 100644 --- a/packages/utils/test/tracing.test.ts +++ b/packages/utils/test/tracing.test.ts @@ -1,9 +1,66 @@ -import { tracingContextFromHeaders } from '../src/tracing'; +import { propagationContextFromHeaders, tracingContextFromHeaders } from '../src/tracing'; + +const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; +const EXAMPLE_BAGGAGE = 'sentry-release=1.2.3,sentry-foo=bar,other=baz'; describe('tracingContextFromHeaders()', () => { it('should produce a frozen baggage (empty object) when there is an incoming trace but no baggage header', () => { + // eslint-disable-next-line deprecation/deprecation const tracingContext = tracingContextFromHeaders('12312012123120121231201212312012-1121201211212012-1', undefined); expect(tracingContext.dynamicSamplingContext).toEqual({}); expect(tracingContext.propagationContext.dsc).toEqual({}); }); }); + +describe('propagationContextFromHeaders()', () => { + it('returns a completely new propagation context when no sentry-trace data is given but baggage data is given', () => { + const result = propagationContextFromHeaders(undefined, undefined); + expect(result).toEqual({ + traceId: expect.any(String), + spanId: expect.any(String), + }); + }); + + it('returns a completely new propagation context when no sentry-trace data is given', () => { + const result = propagationContextFromHeaders(undefined, EXAMPLE_BAGGAGE); + expect(result).toEqual({ + traceId: expect.any(String), + spanId: expect.any(String), + }); + }); + + it('returns the correct traceparent data within the propagation context when sentry trace data is given', () => { + const result = propagationContextFromHeaders(EXAMPLE_SENTRY_TRACE, undefined); + expect(result).toEqual( + expect.objectContaining({ + traceId: '12312012123120121231201212312012', + parentSpanId: '1121201211212012', + spanId: expect.any(String), + sampled: true, + }), + ); + }); + + it('returns a frozen dynamic sampling context (empty object) when there is an incoming trace but no baggage header', () => { + const result = propagationContextFromHeaders(EXAMPLE_SENTRY_TRACE, undefined); + expect(result).toEqual( + expect.objectContaining({ + dsc: {}, + }), + ); + }); + + it('returns the correct trace parent data when both sentry-trace and baggage are given', () => { + const result = propagationContextFromHeaders(EXAMPLE_SENTRY_TRACE, EXAMPLE_BAGGAGE); + expect(result).toEqual({ + traceId: '12312012123120121231201212312012', + parentSpanId: '1121201211212012', + spanId: expect.any(String), + sampled: true, + dsc: { + release: '1.2.3', + foo: 'bar', + }, + }); + }); +}); From 0c16f2159ef471ed9e5ecad89645dbef8743767b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 10:40:49 +0100 Subject: [PATCH 09/68] feat(node): Expose functional integrations to replace classes (#10356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This updates the general (non-tracing) node integrations to be functional. For node & undici, the replacements are slightly different: * `new Http()` --> `httpIntegration()`: In contrast to the class integration, this will create spans by default if tracing is enabled. While at it, this also "fixes" that if `tracing: false` is set, no spans will be created. * `new Undici()` --> `nativeNodeFetchIntegration()`: Renamed this for consistency, and added a `tracing` option similar to http to allow to disable span creation. We can't really deprecate `Integrations.xxx` yet until we have replacements for the tracing integrations 😬 So that would also be a todo left. --- MIGRATION.md | 67 ++++---- .../httpIntegration/spans/scenario.ts | 21 +++ .../tracing-new/httpIntegration/spans/test.ts | 40 +++++ .../httpIntegration/spansDisabled/scenario.ts | 20 +++ .../httpIntegration/spansDisabled/test.ts | 21 +++ .../tracePropagationTargets/scenario.ts | 20 +++ .../tracePropagationTargets/test.ts | 42 +++++ .../scenario.ts | 19 +++ .../tracePropagationTargetsDisabled/test.ts | 42 +++++ packages/astro/src/index.server.ts | 11 ++ packages/bun/src/index.ts | 13 ++ packages/bun/src/sdk.ts | 39 +++-- packages/node-experimental/src/index.ts | 14 +- .../src/integrations/index.ts | 1 + packages/node-experimental/src/sdk/init.ts | 4 +- packages/node/src/index.ts | 22 ++- packages/node/src/integrations/anr/index.ts | 11 +- packages/node/src/integrations/anr/legacy.ts | 1 + packages/node/src/integrations/console.ts | 14 +- packages/node/src/integrations/context.ts | 14 +- .../node/src/integrations/contextlines.ts | 14 +- packages/node/src/integrations/hapi/index.ts | 11 +- packages/node/src/integrations/http.ts | 115 ++++++++++++-- packages/node/src/integrations/index.ts | 2 +- .../src/integrations/local-variables/index.ts | 11 +- .../local-variables/local-variables-async.ts | 12 +- .../local-variables/local-variables-sync.ts | 12 +- packages/node/src/integrations/modules.ts | 14 +- .../src/integrations/onuncaughtexception.ts | 14 +- .../src/integrations/onunhandledrejection.ts | 14 +- packages/node/src/integrations/spotlight.ts | 13 +- .../node/src/integrations/undici/index.ts | 26 ++- packages/node/src/sdk.ts | 62 ++++---- packages/node/test/index.test.ts | 3 +- .../test/integrations/contextlines.test.ts | 5 +- packages/node/test/integrations/http.test.ts | 150 +++++++++++++++++- .../node/test/integrations/spotlight.test.ts | 32 ++-- .../node/test/integrations/undici.test.ts | 76 ++++++++- .../node/test/onuncaughtexception.test.ts | 6 +- .../node/test/onunhandledrejection.test.ts | 6 +- packages/remix/src/index.server.ts | 15 +- packages/serverless/src/index.ts | 13 ++ packages/sveltekit/src/server/index.ts | 15 +- 43 files changed, 913 insertions(+), 164 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/test.ts diff --git a/MIGRATION.md b/MIGRATION.md index f92022cc4690..7dc88f4b78b1 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -34,34 +34,45 @@ integrations from the `Integrations.XXX` hash, is deprecated in favor of using t The following list shows how integrations should be migrated: -| Old | New | Packages | -| ------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `new InboundFilters()` | `inboundFiltersIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new FunctionToString()` | `functionToStringIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new LinkedErrors()` | `linkedErrorsIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new ModuleMetadata()` | `moduleMetadataIntegration()` | `@sentry/core`, `@sentry/browser` | -| `new RequestData()` | `requestDataIntegration()` | `@sentry/core`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | -| `new Wasm() ` | `wasmIntegration()` | `@sentry/wasm` | -| `new Replay()` | `replayIntegration()` | `@sentry/browser` | -| `new ReplayCanvas()` | `replayCanvasIntegration()` | `@sentry/browser` | -| `new Feedback()` | `feedbackIntegration()` | `@sentry/browser` | -| `new CaptureConsole()` | `captureConsoleIntegration()` | `@sentry/integrations` | -| `new Debug()` | `debugIntegration()` | `@sentry/integrations` | -| `new Dedupe()` | `dedupeIntegration()` | `@sentry/browser`, `@sentry/integrations`, `@sentry/deno` | -| `new ExtraErrorData()` | `extraErrorDataIntegration()` | `@sentry/integrations` | -| `new ReportingObserver()` | `reportingObserverIntegration()` | `@sentry/integrations` | -| `new RewriteFrames()` | `rewriteFramesIntegration()` | `@sentry/integrations` | -| `new SessionTiming()` | `sessionTimingIntegration()` | `@sentry/integrations` | -| `new HttpClient()` | `httpClientIntegration()` | `@sentry/integrations` | -| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser`, `@sentry/deno` | -| `new Breadcrumbs()` | `breadcrumbsIntegration()` | `@sentry/browser`, `@sentry/deno` | -| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` , `@sentry/deno` | -| `new HttpContext()` | `httpContextIntegration()` | `@sentry/browser` | -| `new TryCatch()` | `browserApiErrorsIntegration()` | `@sentry/browser`, `@sentry/deno` | -| `new VueIntegration()` | `vueIntegration()` | `@sentry/vue` | -| `new DenoContext()` | `denoContextIntegration()` | `@sentry/deno` | -| `new DenoCron()` | `denoCronIntegration()` | `@sentry/deno` | -| `new NormalizePaths()` | `normalizePathsIntegration()` | `@sentry/deno` | +| Old | New | Packages | +| ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `new InboundFilters()` | `inboundFiltersIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new FunctionToString()` | `functionToStringIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new LinkedErrors()` | `linkedErrorsIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new ModuleMetadata()` | `moduleMetadataIntegration()` | `@sentry/core`, `@sentry/browser` | +| `new RequestData()` | `requestDataIntegration()` | `@sentry/core`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | +| `new Wasm() ` | `wasmIntegration()` | `@sentry/wasm` | +| `new Replay()` | `replayIntegration()` | `@sentry/browser` | +| `new ReplayCanvas()` | `replayCanvasIntegration()` | `@sentry/browser` | +| `new Feedback()` | `feedbackIntegration()` | `@sentry/browser` | +| `new CaptureConsole()` | `captureConsoleIntegration()` | `@sentry/integrations` | +| `new Debug()` | `debugIntegration()` | `@sentry/integrations` | +| `new Dedupe()` | `dedupeIntegration()` | `@sentry/browser`, `@sentry/integrations`, `@sentry/deno` | +| `new ExtraErrorData()` | `extraErrorDataIntegration()` | `@sentry/integrations` | +| `new ReportingObserver()` | `reportingObserverIntegration()` | `@sentry/integrations` | +| `new RewriteFrames()` | `rewriteFramesIntegration()` | `@sentry/integrations` | +| `new SessionTiming()` | `sessionTimingIntegration()` | `@sentry/integrations` | +| `new HttpClient()` | `httpClientIntegration()` | `@sentry/integrations` | +| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser`, `@sentry/node`, `@sentry/deno` | +| `new Breadcrumbs()` | `breadcrumbsIntegration()` | `@sentry/browser`, `@sentry/deno` | +| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` , `@sentry/deno` | +| `new HttpContext()` | `httpContextIntegration()` | `@sentry/browser` | +| `new TryCatch()` | `browserApiErrorsIntegration()` | `@sentry/browser`, `@sentry/deno` | +| `new VueIntegration()` | `vueIntegration()` | `@sentry/vue` | +| `new DenoContext()` | `denoContextIntegration()` | `@sentry/deno` | +| `new DenoCron()` | `denoCronIntegration()` | `@sentry/deno` | +| `new NormalizePaths()` | `normalizePathsIntegration()` | `@sentry/deno` | +| `new Console()` | `consoleIntegration()` | `@sentry/node` | +| `new Context()` | `nodeContextIntegration()` | `@sentry/node` | +| `new Modules()` | `modulesIntegration()` | `@sentry/node` | +| `new OnUncaughtException()` | `onUncaughtExceptionIntegration()` | `@sentry/node` | +| `new OnUnhandledRejection()` | `onUnhandledRejectionIntegration()` | `@sentry/node` | +| `new LocalVariables()` | `localVariablesIntegration()` | `@sentry/node` | +| `new Spotlight()` | `spotlightIntergation()` | `@sentry/node` | +| `new Anr()` | `anrIntergation()` | `@sentry/node` | +| `new Hapi()` | `hapiIntegration()` | `@sentry/node` | +| `new Undici()` | `nativeNodeFetchIntegration()` | `@sentry/node` | +| `new Http()` | `httpIntegration()` | `@sentry/node` | ## Deprecate `hub.bindClient()` and `makeMain()` diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/scenario.ts new file mode 100644 index 000000000000..9b1abf466db1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/scenario.ts @@ -0,0 +1,21 @@ +import '@sentry/tracing'; + +import * as http from 'http'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [Sentry.httpIntegration({})], + debug: true, +}); + +// 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)); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/test.ts new file mode 100644 index 000000000000..bd95db22de6e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spans/test.ts @@ -0,0 +1,40 @@ +import nock from 'nock'; + +import { TestEnv, assertSentryTransaction } from '../../../../utils'; + +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', + tags: { + 'http.status_code': '200', + }, + }, + { + description: 'GET http://match-this-url.com/api/v1', + op: 'http.client', + origin: 'auto.http.node.http', + status: 'ok', + tags: { + 'http.status_code': '200', + }, + }, + ], + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/scenario.ts new file mode 100644 index 000000000000..61711e974f7d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/scenario.ts @@ -0,0 +1,20 @@ +import '@sentry/tracing'; + +import * as http from 'http'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [Sentry.httpIntegration({ tracing: false })], +}); + +// 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)); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/test.ts new file mode 100644 index 000000000000..bacf5eaf1882 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/spansDisabled/test.ts @@ -0,0 +1,21 @@ +import nock from 'nock'; + +import { TestEnv, assertSentryTransaction } from '../../../../utils'; + +test('should not capture spans for outgoing http requests if tracing is disabled', 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: [], + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/scenario.ts new file mode 100644 index 000000000000..7794b20911f9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/scenario.ts @@ -0,0 +1,20 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import '@sentry/tracing'; + +import * as http from 'http'; +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({})], +}); + +Sentry.startSpan({ name: 'test_transaction' }, () => { + http.get('http://match-this-url.com/api/v0'); + http.get('http://match-this-url.com/api/v1'); + http.get('http://dont-match-this-url.com/api/v2'); + http.get('http://dont-match-this-url.com/api/v3'); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/test.ts new file mode 100644 index 000000000000..59e4eff9e105 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargets/test.ts @@ -0,0 +1,42 @@ +import nock from 'nock'; + +import { TestEnv, runScenario } from '../../../../utils'; + +test('httpIntegration should instrument correct requests when tracePropagationTargets option is provided & tracing is enabled', 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); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/scenario.ts new file mode 100644 index 000000000000..c04616f7db89 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/scenario.ts @@ -0,0 +1,19 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import '@sentry/tracing'; + +import * as http from 'http'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [Sentry.httpIntegration({})], +}); + +Sentry.startSpan({ name: 'test_transaction' }, () => { + http.get('http://match-this-url.com/api/v0'); + http.get('http://match-this-url.com/api/v1'); + http.get('http://dont-match-this-url.com/api/v2'); + http.get('http://dont-match-this-url.com/api/v3'); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/test.ts new file mode 100644 index 000000000000..abc1ff025b78 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-new/httpIntegration/tracePropagationTargetsDisabled/test.ts @@ -0,0 +1,42 @@ +import nock from 'nock'; + +import { TestEnv, runScenario } from '../../../../utils'; + +test('httpIntegration should not instrument when tracing is enabled', async () => { + const match1 = nock('http://match-this-url.com') + .get('/api/v0') + .matchHeader('baggage', val => val === undefined) + .matchHeader('sentry-trace', val => val === undefined) + .reply(200); + + const match2 = nock('http://match-this-url.com') + .get('/api/v1') + .matchHeader('baggage', val => val === undefined) + .matchHeader('sentry-trace', val => val === undefined) + .reply(200); + + const match3 = nock('http://dont-match-this-url.com') + .get('/api/v2') + .matchHeader('baggage', val => val === undefined) + .matchHeader('sentry-trace', val => val === undefined) + .reply(200); + + const match4 = nock('http://dont-match-this-url.com') + .get('/api/v3') + .matchHeader('baggage', val => val === undefined) + .matchHeader('sentry-trace', val => val === undefined) + .reply(200); + + const env = await TestEnv.init(__dirname); + await runScenario(env.url); + + env.server.close(); + nock.cleanAll(); + + await new Promise(resolve => env.server.close(resolve)); + + expect(match1.isDone()).toBe(true); + expect(match2.isDone()).toBe(true); + expect(match3.isDone()).toBe(true); + expect(match4.isDone()).toBe(true); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 8d4bb2a7e371..50a77fff599c 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -70,6 +70,17 @@ export { // eslint-disable-next-line deprecation/deprecation deepReadDirSync, Integrations, + consoleIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + modulesIntegration, + contextLinesIntegration, + nodeContextIntegration, + localVariablesIntegration, + requestDataIntegration, + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, Handlers, setMeasurement, getActiveSpan, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 5742597485e0..d793a1a93551 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -104,6 +104,18 @@ export { extractRequestData, getSentryRelease, addRequestDataToEvent, + anrIntegration, + consoleIntegration, + contextLinesIntegration, + hapiIntegration, + httpIntegration, + localVariablesIntegration, + modulesIntegration, + nativeNodeFetchintegration, + nodeContextIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + spotlightIntegration, } from '@sentry/node'; export { BunClient } from './client'; @@ -122,6 +134,7 @@ import * as BunIntegrations from './integrations'; const INTEGRATIONS = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, + // eslint-disable-next-line deprecation/deprecation ...NodeIntegrations, ...BunIntegrations, }; diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index ab637bc7a59e..74150b72ef6f 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -1,6 +1,19 @@ /* eslint-disable max-lines */ -import { FunctionToString, InboundFilters, LinkedErrors } from '@sentry/core'; -import { Integrations as NodeIntegrations, init as initNode } from '@sentry/node'; +import { + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, + requestDataIntegration, +} from '@sentry/core'; +import { + consoleIntegration, + contextLinesIntegration, + httpIntegration, + init as initNode, + modulesIntegration, + nativeNodeFetchintegration, + nodeContextIntegration, +} from '@sentry/node'; import type { Integration, Options } from '@sentry/types'; import { BunClient } from './client'; @@ -10,25 +23,23 @@ import type { BunOptions } from './types'; /** @deprecated Use `getDefaultIntegrations(options)` instead. */ export const defaultIntegrations = [ - /* eslint-disable deprecation/deprecation */ // Common - new InboundFilters(), - new FunctionToString(), - new LinkedErrors(), - /* eslint-enable deprecation/deprecation */ + inboundFiltersIntegration(), + functionToStringIntegration(), + linkedErrorsIntegration(), + requestDataIntegration(), // Native Wrappers - new NodeIntegrations.Console(), - new NodeIntegrations.Http(), - new NodeIntegrations.Undici(), + consoleIntegration(), + httpIntegration(), + nativeNodeFetchintegration(), // Global Handlers # TODO (waiting for https://github.com/oven-sh/bun/issues/5091) // new NodeIntegrations.OnUncaughtException(), // new NodeIntegrations.OnUnhandledRejection(), // Event Info - new NodeIntegrations.ContextLines(), + contextLinesIntegration(), // new NodeIntegrations.LocalVariables(), # does't work with Bun - new NodeIntegrations.Context(), - new NodeIntegrations.Modules(), - new NodeIntegrations.RequestData(), + nodeContextIntegration(), + modulesIntegration(), // Bun Specific new BunServer(), ]; diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 2fcb4ee1b166..3b208ea1d2d8 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -2,14 +2,13 @@ import { Integrations as CoreIntegrations } from '@sentry/core'; import * as NodeExperimentalIntegrations from './integrations'; -const INTEGRATIONS = { +export const Integrations = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, ...NodeExperimentalIntegrations, }; export { init } from './sdk/init'; -export { INTEGRATIONS as Integrations }; export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations'; export * as Handlers from './sdk/handlers'; export type { Span } from './types'; @@ -74,6 +73,17 @@ export { captureCheckIn, withMonitor, hapiErrorPlugin, + consoleIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + modulesIntegration, + contextLinesIntegration, + nodeContextIntegration, + localVariablesIntegration, + requestDataIntegration, + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, } from '@sentry/node'; export type { diff --git a/packages/node-experimental/src/integrations/index.ts b/packages/node-experimental/src/integrations/index.ts index b625872d2fb7..7279f45c2dfc 100644 --- a/packages/node-experimental/src/integrations/index.ts +++ b/packages/node-experimental/src/integrations/index.ts @@ -9,6 +9,7 @@ const { Context, RequestData, LocalVariables, + // eslint-disable-next-line deprecation/deprecation } = NodeIntegrations; export { diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index c0ce8056e336..8472bcf17d6e 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,11 +1,11 @@ import { endSession, getIntegrationsToSetup, hasTracingEnabled, startSession } from '@sentry/core'; import { - Integrations, defaultIntegrations as defaultNodeIntegrations, defaultStackParser, getDefaultIntegrations as getDefaultNodeIntegrations, getSentryRelease, makeNodeTransport, + spotlightIntegration, } from '@sentry/node'; import type { Client, Integration, Options } from '@sentry/types'; import { @@ -94,7 +94,7 @@ export function init(options: NodeExperimentalOptions | undefined = {}): void { client.addIntegration(integration); } client.addIntegration( - new Integrations.Spotlight({ + spotlightIntegration({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, }), ); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 79edd5eddd89..fc0edc005400 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -122,22 +122,36 @@ import * as Handlers from './handlers'; import * as NodeIntegrations from './integrations'; import * as TracingIntegrations from './tracing/integrations'; -const INTEGRATIONS = { +// TODO: Deprecate this once we migrated tracing integrations +export const Integrations = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, ...NodeIntegrations, ...TracingIntegrations, }; +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 { Undici } from './integrations/undici'; -export { Http } from './integrations/http'; // --- -export { INTEGRATIONS as Integrations, Handlers }; +export { Handlers }; export { hapiErrorPlugin } from './integrations/hapi'; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 81b45bf5bf03..91deb2259e72 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,6 +1,6 @@ // TODO (v8): This import can be removed once we only support Node with global URL import { URL } from 'url'; -import { convertIntegrationFnToClass, getCurrentScope } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core'; import type { Client, Contexts, Event, EventHint, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { dynamicRequire, logger } from '@sentry/utils'; import type { Worker, WorkerOptions } from 'worker_threads'; @@ -52,7 +52,7 @@ interface InspectorApi { const INTEGRATION_NAME = 'Anr'; -const anrIntegration = ((options: Partial = {}) => { +const _anrIntegration = ((options: Partial = {}) => { return { name: INTEGRATION_NAME, // TODO v8: Remove this @@ -68,10 +68,14 @@ const anrIntegration = ((options: Partial = {}) => { }; }) satisfies IntegrationFn; +export const anrIntegration = defineIntegration(_anrIntegration); + /** * Starts a thread to detect App Not Responding (ANR) events * * ANR detection requires Node 16.17.0 or later + * + * @deprecated Use `anrIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration) as IntegrationClass< @@ -80,6 +84,9 @@ export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration) new (options?: Partial): Integration & { setup(client: Client): void }; }; +// eslint-disable-next-line deprecation/deprecation +export type Anr = typeof Anr; + /** * Starts the ANR worker thread */ diff --git a/packages/node/src/integrations/anr/legacy.ts b/packages/node/src/integrations/anr/legacy.ts index 1d1ebc3024e3..d8b4ff1bc6dc 100644 --- a/packages/node/src/integrations/anr/legacy.ts +++ b/packages/node/src/integrations/anr/legacy.ts @@ -26,6 +26,7 @@ interface LegacyOptions { */ export function enableAnrDetection(options: Partial): Promise { const client = getClient() as NodeClient; + // eslint-disable-next-line deprecation/deprecation const integration = new Anr(options); integration.setup(client); return Promise.resolve(); diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 1d4e0182e59a..aacf3447ea2a 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,11 +1,11 @@ import * as util from 'util'; -import { addBreadcrumb, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import { addBreadcrumb, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; const INTEGRATION_NAME = 'Console'; -const consoleIntegration = (() => { +const _consoleIntegration = (() => { return { name: INTEGRATION_NAME, // TODO v8: Remove this @@ -32,8 +32,16 @@ const consoleIntegration = (() => { }; }) satisfies IntegrationFn; -/** Console module integration */ +export const consoleIntegration = defineIntegration(_consoleIntegration); + +/** + * Console module integration. + * @deprecated Use `consoleIntegration()` instead. + */ // 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; diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index 058ce40b4c11..db712f4ea95d 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -4,7 +4,7 @@ import { readFile, readdir } from 'fs'; import * as os from 'os'; import { join } from 'path'; import { promisify } from 'util'; -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { AppContext, CloudResourceContext, @@ -37,7 +37,7 @@ interface ContextOptions { cloudResource?: boolean; } -const nodeContextIntegration = ((options: ContextOptions = {}) => { +const _nodeContextIntegration = ((options: ContextOptions = {}) => { let cachedContext: Promise | undefined; const _options = { @@ -110,7 +110,12 @@ const nodeContextIntegration = ((options: ContextOptions = {}) => { }; }) satisfies IntegrationFn; -/** Add node modules / packages to the event */ +export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); + +/** + * Add node modules / packages to the event. + * @deprecated Use `nodeContextIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const Context = convertIntegrationFnToClass(INTEGRATION_NAME, nodeContextIntegration) as IntegrationClass< Integration & { processEvent: (event: Event) => Promise } @@ -124,6 +129,9 @@ export const Context = convertIntegrationFnToClass(INTEGRATION_NAME, nodeContext }): Integration; }; +// eslint-disable-next-line deprecation/deprecation +export type Context = typeof Context; + /** * 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 eccc80f7527a..5e98c7cbb813 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,5 +1,5 @@ import { readFile } from 'fs'; -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; @@ -35,7 +35,7 @@ interface ContextLinesOptions { frameContextLines?: number; } -const contextLinesIntegration = ((options: ContextLinesOptions = {}) => { +const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; return { @@ -48,7 +48,12 @@ const contextLinesIntegration = ((options: ContextLinesOptions = {}) => { }; }) satisfies IntegrationFn; -/** Add node modules / packages to the event */ +export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); + +/** + * Add node modules / packages to the event. + * @deprecated Use `contextLinesIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration) as IntegrationClass< Integration & { processEvent: (event: Event) => Promise } @@ -119,6 +124,9 @@ function addSourceContextToFrames(frames: StackFrame[], contextLines: number): v } } +// eslint-disable-next-line deprecation/deprecation +export type ContextLines = typeof ContextLines; + /** * 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. diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index c80265926ce4..82f8737721a9 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -3,6 +3,7 @@ import { captureException, continueTrace, convertIntegrationFnToClass, + defineIntegration, getActiveTransaction, getCurrentScope, getDynamicSamplingContextFromSpan, @@ -139,7 +140,7 @@ export type HapiOptions = { const INTEGRATION_NAME = 'Hapi'; -const hapiIntegration = ((options: HapiOptions = {}) => { +const _hapiIntegration = ((options: HapiOptions = {}) => { const server = options.server as undefined | Server; return { @@ -161,8 +162,14 @@ const hapiIntegration = ((options: HapiOptions = {}) => { }; }) satisfies IntegrationFn; +export const hapiIntegration = defineIntegration(_hapiIntegration); + /** - * Hapi Framework Integration + * 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/http.ts b/packages/node/src/integrations/http.ts index de013541257e..f572fcc160f2 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,7 +1,8 @@ +/* eslint-disable max-lines */ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; -import { getIsolationScope } from '@sentry/core'; +import { defineIntegration, getIsolationScope, hasTracingEnabled } from '@sentry/core'; import { addBreadcrumb, getActiveSpan, @@ -15,9 +16,18 @@ import { spanToJSON, spanToTraceHeader, } from '@sentry/core'; -import type { EventProcessor, Integration, SanitizedRequestData, TracePropagationTargets } from '@sentry/types'; +import type { + ClientOptions, + EventProcessor, + Integration, + IntegrationFn, + IntegrationFnResult, + SanitizedRequestData, + TracePropagationTargets, +} from '@sentry/types'; import { LRUMap, + dropUndefinedKeys, dynamicSamplingContextToSentryBaggageHeader, fill, generateSentryTraceHeader, @@ -28,6 +38,7 @@ import { 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 { cleanSpanDescription, extractRawUrl, extractUrl, normalizeRequestArgs } from './utils/http'; @@ -56,6 +67,12 @@ interface TracingOptions { * 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; } interface HttpOptions { @@ -72,9 +89,59 @@ interface HttpOptions { tracing?: TracingOptions | boolean; } +/* These are the newer options for `httpIntegration`. */ +interface HttpIntegrationOptions { + /** + * 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; +} + +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 IntegrationFnResult; +}) 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 module integration instruments Node's internal http module. It creates breadcrumbs, transactions for outgoing * http requests and attaches trace data when tracing is enabled via its `tracing` option. + * + * @deprecated Use `httpIntegration()` instead. */ export class Http implements Integration { /** @@ -85,6 +152,7 @@ export class Http implements Integration { /** * @inheritDoc */ + // eslint-disable-next-line deprecation/deprecation public name: string = Http.id; private readonly _breadcrumbs: boolean; @@ -105,23 +173,26 @@ export class Http implements Integration { _addGlobalEventProcessor: (callback: EventProcessor) => void, setupOnceGetCurrentHub: () => Hub, ): void { + // eslint-disable-next-line deprecation/deprecation + const clientOptions = setupOnceGetCurrentHub().getClient()?.getOptions(); + + // 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); + // No need to instrument if we don't want to track anything - if (!this._breadcrumbs && !this._tracing) { + if (!this._breadcrumbs && !shouldCreateSpans) { return; } - // eslint-disable-next-line deprecation/deprecation - const clientOptions = setupOnceGetCurrentHub().getClient()?.getOptions(); - // Do not auto-instrument for other instrumenter if (clientOptions && clientOptions.instrumenter !== 'sentry') { DEBUG_BUILD && logger.log('HTTP Integration is skipped because of instrumenter configuration.'); return; } - const shouldCreateSpanForRequest = - // eslint-disable-next-line deprecation/deprecation - this._tracing?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest; + const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions); + // eslint-disable-next-line deprecation/deprecation const tracePropagationTargets = clientOptions?.tracePropagationTargets || this._tracing?.tracePropagationTargets; @@ -389,3 +460,29 @@ function normalizeBaggageHeader( // 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; +} diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index 3e1a60f6951f..12f57116c533 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ export { Console } from './console'; export { Http } from './http'; export { OnUncaughtException } from './onuncaughtexception'; @@ -5,7 +6,6 @@ export { OnUnhandledRejection } from './onunhandledrejection'; export { Modules } from './modules'; export { ContextLines } from './contextlines'; export { Context } from './context'; -// eslint-disable-next-line deprecation/deprecation export { RequestData } from '@sentry/core'; export { LocalVariables } from './local-variables'; export { Undici } from './undici'; diff --git a/packages/node/src/integrations/local-variables/index.ts b/packages/node/src/integrations/local-variables/index.ts index 708b4b41ea24..79cf3b0a7d67 100644 --- a/packages/node/src/integrations/local-variables/index.ts +++ b/packages/node/src/integrations/local-variables/index.ts @@ -1,6 +1,13 @@ -import { LocalVariablesSync } from './local-variables-sync'; +import { LocalVariablesSync, localVariablesSyncIntegration } from './local-variables-sync'; /** - * Adds local variables to exception frames + * Adds local variables to exception frames. + * + * @deprecated Use `localVariablesIntegration()` instead. */ +// eslint-disable-next-line deprecation/deprecation export const LocalVariables = LocalVariablesSync; +// eslint-disable-next-line deprecation/deprecation +export type LocalVariables = LocalVariablesSync; + +export const localVariablesIntegration = localVariablesSyncIntegration; diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts index 76df88760bc9..b5f015b2b7db 100644 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node/src/integrations/local-variables/local-variables-async.ts @@ -1,5 +1,5 @@ import type { Session } from 'node:inspector/promises'; -import { convertIntegrationFnToClass } from '@sentry/core'; +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'; @@ -76,7 +76,7 @@ const INTEGRATION_NAME = 'LocalVariablesAsync'; /** * Adds local variables to exception frames */ -const localVariablesAsyncIntegration = ((options: LocalVariablesIntegrationOptions = {}) => { +const _localVariablesAsyncIntegration = ((options: LocalVariablesIntegrationOptions = {}) => { const cachedFrames: LRUMap = new LRUMap(20); let rateLimiter: RateLimitIncrement | undefined; let shouldProcessEvent = false; @@ -253,11 +253,17 @@ const localVariablesAsyncIntegration = ((options: LocalVariablesIntegrationOptio }; }) satisfies IntegrationFn; +export const localVariablesAsyncIntegration = defineIntegration(_localVariablesAsyncIntegration); + /** - * Adds local variables to exception frames + * 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 f8d803a76c71..72f1add2748b 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { convertIntegrationFnToClass, getClient } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; @@ -219,7 +219,7 @@ const INTEGRATION_NAME = 'LocalVariables'; /** * Adds local variables to exception frames */ -const localVariablesSyncIntegration = (( +const _localVariablesSyncIntegration = (( options: LocalVariablesIntegrationOptions = {}, session: DebugSession | undefined = tryNewAsyncSession(), ) => { @@ -392,8 +392,11 @@ const localVariablesSyncIntegration = (( }; }) satisfies IntegrationFn; +export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); + /** - * Adds local variables to exception frames + * Adds local variables to exception frames. + * @deprecated Use `localVariablesSyncIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const LocalVariablesSync = convertIntegrationFnToClass( @@ -402,3 +405,6 @@ export const LocalVariablesSync = convertIntegrationFnToClass( ) as IntegrationClass Event; setup: (client: NodeClient) => void }> & { new (options?: LocalVariablesIntegrationOptions, session?: DebugSession): Integration; }; + +// eslint-disable-next-line deprecation/deprecation +export type LocalVariablesSync = typeof LocalVariablesSync; diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index e21a92e770d7..008376670724 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; let moduleCache: { [key: string]: string }; @@ -76,7 +76,7 @@ function _getModules(): { [key: string]: string } { return moduleCache; } -const modulesIntegration = (() => { +const _modulesIntegration = (() => { return { name: INTEGRATION_NAME, // TODO v8: Remove this @@ -92,8 +92,16 @@ const modulesIntegration = (() => { }; }) satisfies IntegrationFn; -/** Add node modules / packages to the event */ +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/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index a3346f6153d5..0eb79833ddbf 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,4 +1,4 @@ -import { captureException, convertIntegrationFnToClass } from '@sentry/core'; +import { captureException, convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import { getClient } from '@sentry/core'; import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -40,7 +40,7 @@ interface OnUncaughtExceptionOptions { const INTEGRATION_NAME = 'OnUncaughtException'; -const onUncaughtExceptionIntegration = ((options: Partial = {}) => { +const _onUncaughtExceptionIntegration = ((options: Partial = {}) => { const _options = { exitEvenIfOtherHandlersAreRegistered: true, ...options, @@ -56,7 +56,12 @@ const onUncaughtExceptionIntegration = ((options: Partial void); /** Exported only for tests */ diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 45fa5a61ca57..0c44c0e983c0 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,4 +1,4 @@ -import { captureException, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import { captureException, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; @@ -16,7 +16,7 @@ interface OnUnhandledRejectionOptions { const INTEGRATION_NAME = 'OnUnhandledRejection'; -const onUnhandledRejectionIntegration = ((options: Partial = {}) => { +const _onUnhandledRejectionIntegration = ((options: Partial = {}) => { const mode = options.mode || 'warn'; return { @@ -29,7 +29,12 @@ const onUnhandledRejectionIntegration = ((options: Partial): Integration; }; +// eslint-disable-next-line deprecation/deprecation +export type OnUnhandledRejection = typeof OnUnhandledRejection; + /** * Send an exception with reason * @param reason string diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index ab27f860c97b..52b8941daa71 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -1,6 +1,6 @@ import * as http from 'http'; import { URL } from 'url'; -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Client, Envelope, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; @@ -14,7 +14,7 @@ type SpotlightConnectionOptions = { const INTEGRATION_NAME = 'Spotlight'; -const spotlightIntegration = ((options: Partial = {}) => { +const _spotlightIntegration = ((options: Partial = {}) => { const _options = { sidecarUrl: options.sidecarUrl || 'http://localhost:8969/stream', }; @@ -32,12 +32,16 @@ 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 + * 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< @@ -50,6 +54,9 @@ export const Spotlight = convertIntegrationFnToClass(INTEGRATION_NAME, spotlight ): Integration; }; +// eslint-disable-next-line deprecation/deprecation +export type Spotlight = typeof Spotlight; + function connectToSpotlight(client: Client, options: Required): void { const spotlightUrl = parseSidecarUrl(options.sidecarUrl); if (!spotlightUrl) { diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 31178d95aaa8..d53533699104 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,16 +1,18 @@ import { addBreadcrumb, + defineIntegration, getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, getIsolationScope, + hasTracingEnabled, isSentryRequestUrl, setHttpStatus, spanToTraceHeader, } from '@sentry/core'; -import type { EventProcessor, Integration, Span } from '@sentry/types'; +import type { EventProcessor, Integration, IntegrationFn, IntegrationFnResult, Span } from '@sentry/types'; import { LRUMap, dynamicRequire, @@ -44,6 +46,13 @@ export interface UndiciOptions { * 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. @@ -64,6 +73,13 @@ export interface UndiciOptions { // 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 IntegrationFnResult; +}) satisfies IntegrationFn; + +export const nativeNodeFetchintegration = defineIntegration(_nativeNodeFetchintegration); + /** * Instruments outgoing HTTP requests made with the `undici` package via * Node's `diagnostics_channel` API. @@ -71,6 +87,8 @@ export interface UndiciOptions { * Supports Undici 4.7.0 or higher. * * Requires Node 16.17.0 or higher. + * + * @deprecated Use `nativeNodeFetchintegration()` instead. */ export class Undici implements Integration { /** @@ -81,6 +99,7 @@ export class Undici implements Integration { /** * @inheritDoc */ + // eslint-disable-next-line deprecation/deprecation public name: string = Undici.id; private readonly _options: UndiciOptions; @@ -91,6 +110,7 @@ export class Undici implements Integration { public constructor(_options: Partial = {}) { this._options = { breadcrumbs: _options.breadcrumbs === undefined ? true : _options.breadcrumbs, + tracing: _options.tracing, shouldCreateSpanForRequest: _options.shouldCreateSpanForRequest, }; } @@ -124,6 +144,10 @@ export class Undici implements Integration { /** 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; } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 8584f66dc083..01825a404e20 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,15 +1,16 @@ /* eslint-disable max-lines */ import { - FunctionToString, - InboundFilters, - LinkedErrors, endSession, + functionToStringIntegration, getClient, getCurrentScope, getIntegrationsToSetup, getIsolationScope, getMainCarrier, + inboundFiltersIntegration, initAndBind, + linkedErrorsIntegration, + requestDataIntegration, startSession, } from '@sentry/core'; import type { Integration, Options, SessionStatus, StackParser } from '@sentry/types'; @@ -23,46 +24,39 @@ import { import { setNodeAsyncContextStrategy } from './async'; import { NodeClient } from './client'; -import { - Console, - Context, - ContextLines, - Http, - LocalVariables, - Modules, - OnUncaughtException, - OnUnhandledRejection, - RequestData, - Spotlight, - Undici, -} from './integrations'; +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'; /** @deprecated Use `getDefaultIntegrations(options)` instead. */ - export const defaultIntegrations = [ - /* eslint-disable deprecation/deprecation */ // Common - new InboundFilters(), - new FunctionToString(), - new LinkedErrors(), - /* eslint-enable deprecation/deprecation */ + inboundFiltersIntegration(), + functionToStringIntegration(), + linkedErrorsIntegration(), + requestDataIntegration(), // Native Wrappers - new Console(), - new Http(), - new Undici(), + consoleIntegration(), + httpIntegration(), + nativeNodeFetchintegration(), // Global Handlers - new OnUncaughtException(), - new OnUnhandledRejection(), + onUncaughtExceptionIntegration(), + onUnhandledRejectionIntegration(), // Event Info - new ContextLines(), - new LocalVariables(), - new Context(), - new Modules(), - // eslint-disable-next-line deprecation/deprecation - new RequestData(), + contextLinesIntegration(), + localVariablesIntegration(), + nodeContextIntegration(), + modulesIntegration(), ]; /** Get the default integrations for the Node SDK. */ @@ -201,7 +195,7 @@ export function init(options: NodeOptions = {}): void { client.addIntegration(integration); } client.addIntegration( - new Spotlight({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined }), + spotlightIntegration({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined }), ); } } diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 2312cf8ce981..b0b0000839f2 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -10,6 +10,7 @@ import type { EventHint, Integration } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; import type { Event } from '../src'; +import { contextLinesIntegration } from '../src'; import { NodeClient, addBreadcrumb, @@ -193,7 +194,7 @@ describe('SentryNode', () => { stackParser: defaultStackParser, beforeSend, dsn, - integrations: [new ContextLines()], + integrations: [contextLinesIntegration()], }); const client = new NodeClient(options); setCurrentClient(client); diff --git a/packages/node/test/integrations/contextlines.test.ts b/packages/node/test/integrations/contextlines.test.ts index dda78689e711..dccc6c113b5f 100644 --- a/packages/node/test/integrations/contextlines.test.ts +++ b/packages/node/test/integrations/contextlines.test.ts @@ -16,6 +16,7 @@ describe('ContextLines', () => { beforeEach(() => { readFileSpy = jest.spyOn(fs, 'readFile'); + // eslint-disable-next-line deprecation/deprecation contextLines = new ContextLines(); resetFileContentCache(); }); @@ -98,6 +99,7 @@ describe('ContextLines', () => { }); test('parseStack with no context', async () => { + // eslint-disable-next-line deprecation/deprecation contextLines = new ContextLines({ frameContextLines: 0 }); expect.assertions(1); @@ -110,7 +112,8 @@ describe('ContextLines', () => { test('does not attempt to readfile multiple times if it fails', async () => { expect.assertions(1); - contextLines = new ContextLines({}); + // eslint-disable-next-line deprecation/deprecation + contextLines = new ContextLines(); readFileSpy.mockImplementation(() => { throw new Error("ENOENT: no such file or directory, open '/does/not/exist.js'"); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index a1f4d38b3da5..0b1d81edd29c 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -11,7 +11,12 @@ import { HttpsProxyAgent } from '../../src/proxy'; import type { Breadcrumb } from '../../src'; import { NodeClient } from '../../src/client'; -import { Http as HttpIntegration } from '../../src/integrations/http'; +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'; @@ -55,15 +60,18 @@ describe('tracing', () => { 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); - const hub = new Hub(client); + const hub = new Hub(); // eslint-disable-next-line deprecation/deprecation makeMain(hub); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); } it("creates a span for each outgoing non-sentry request when there's a transaction on the scope", () => { @@ -256,6 +264,7 @@ describe('tracing', () => { 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', @@ -263,6 +272,7 @@ describe('tracing', () => { }); const hub = new Hub(new NodeClient(options)); + // eslint-disable-next-line deprecation/deprecation const integration = new HttpIntegration(); integration.setupOnce( () => {}, @@ -381,6 +391,7 @@ describe('tracing', () => { 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 }); const hub = createHub({ shouldCreateSpanForRequest: () => false }); @@ -428,6 +439,7 @@ describe('tracing', () => { (url, tracePropagationTargets) => { nock(url).get(/.*/).reply(200); + // eslint-disable-next-line deprecation/deprecation const httpIntegration = new HttpIntegration({ tracing: true }); const hub = createHub({ tracePropagationTargets }); @@ -460,6 +472,7 @@ describe('tracing', () => { (url, tracePropagationTargets) => { nock(url).get(/.*/).reply(200); + // eslint-disable-next-line deprecation/deprecation const httpIntegration = new HttpIntegration({ tracing: true }); const hub = createHub({ tracePropagationTargets }); @@ -484,6 +497,7 @@ describe('tracing', () => { 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, @@ -535,6 +549,7 @@ describe('tracing', () => { (url, tracePropagationTargets) => { nock(url).get(/.*/).reply(200); + // eslint-disable-next-line deprecation/deprecation const httpIntegration = new HttpIntegration({ tracing: { tracePropagationTargets } }); const hub = createHub(); @@ -567,6 +582,7 @@ describe('tracing', () => { (url, tracePropagationTargets) => { nock(url).get(/.*/).reply(200); + // eslint-disable-next-line deprecation/deprecation const httpIntegration = new HttpIntegration({ tracing: { tracePropagationTargets } }); const hub = createHub(); @@ -596,6 +612,7 @@ describe('default protocols', () => { }); 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)) { @@ -688,3 +705,132 @@ describe('default protocols', () => { 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); + const hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + + 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 () { + const hub = new Hub(); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + + 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 actual = _shouldCreateSpans(tracing, clientOptions); + expect(actual).toEqual(expected); + }); +}); + +describe('_getShouldCreateSpanForRequest', () => { + beforeEach(function () { + const hub = new Hub(); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + + 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/spotlight.test.ts b/packages/node/test/integrations/spotlight.test.ts index 266d64b5710a..756170d89518 100644 --- a/packages/node/test/integrations/spotlight.test.ts +++ b/packages/node/test/integrations/spotlight.test.ts @@ -2,7 +2,7 @@ import * as http from 'http'; import type { Envelope, EventEnvelope } from '@sentry/types'; import { createEnvelope, logger } from '@sentry/utils'; -import { NodeClient } from '../../src'; +import { NodeClient, spotlightIntegration } from '../../src'; import { Spotlight } from '../../src/integrations'; import { getDefaultNodeClientOptions } from '../helper/node-client-options'; @@ -18,8 +18,10 @@ describe('Spotlight', () => { const client = new NodeClient(options); it('has a name and id', () => { + // eslint-disable-next-line deprecation/deprecation const integration = new Spotlight(); expect(integration.name).toEqual('Spotlight'); + // eslint-disable-next-line deprecation/deprecation expect(Spotlight.id).toEqual('Spotlight'); }); @@ -28,7 +30,7 @@ describe('Spotlight', () => { ...client, on: jest.fn(), }; - const integration = new Spotlight(); + const integration = spotlightIntegration(); // @ts-expect-error - this is fine in tests integration.setup(clientWithSpy); expect(clientWithSpy.on).toHaveBeenCalledWith('beforeEnvelope', expect.any(Function)); @@ -49,7 +51,7 @@ describe('Spotlight', () => { on: jest.fn().mockImplementationOnce((_, cb) => (callback = cb)), }; - const integration = new Spotlight(); + const integration = spotlightIntegration(); // @ts-expect-error - this is fine in tests integration.setup(clientWithSpy); @@ -88,7 +90,7 @@ describe('Spotlight', () => { on: jest.fn().mockImplementationOnce((_, cb) => (callback = cb)), }; - const integration = new Spotlight({ sidecarUrl: 'http://mylocalhost:8888/abcd' }); + const integration = spotlightIntegration({ sidecarUrl: 'http://mylocalhost:8888/abcd' }); // @ts-expect-error - this is fine in tests integration.setup(clientWithSpy); @@ -114,13 +116,13 @@ describe('Spotlight', () => { describe('no-ops if', () => { it('an invalid URL is passed', () => { - const integration = new Spotlight({ sidecarUrl: 'invalid-url' }); - integration.setup(client); + const integration = spotlightIntegration({ sidecarUrl: 'invalid-url' }); + integration.setup!(client); expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid sidecar URL: invalid-url')); }); it("the client doesn't support life cycle hooks", () => { - const integration = new Spotlight({ sidecarUrl: 'http://mylocalhost:8969' }); + const integration = spotlightIntegration({ sidecarUrl: 'http://mylocalhost:8969' }); const clientWithoutHooks = { ...client }; // @ts-expect-error - this is fine in tests delete client.on; @@ -134,8 +136,8 @@ describe('Spotlight', () => { const oldEnvValue = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; - const integration = new Spotlight({ sidecarUrl: 'http://localhost:8969' }); - integration.setup(client); + 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?"), @@ -148,8 +150,8 @@ describe('Spotlight', () => { const oldEnvValue = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; - const integration = new Spotlight({ sidecarUrl: 'http://localhost:8969' }); - integration.setup(client); + 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?"), @@ -164,8 +166,8 @@ describe('Spotlight', () => { // @ts-expect-error - TS complains but we explicitly wanna test this delete global.process; - const integration = new Spotlight({ sidecarUrl: 'http://localhost:8969' }); - integration.setup(client); + 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?"), @@ -180,8 +182,8 @@ describe('Spotlight', () => { // @ts-expect-error - TS complains but we explicitly wanna test this delete process.env; - const integration = new Spotlight({ sidecarUrl: 'http://localhost:8969' }); - integration.setup(client); + 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?"), diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index abda44026fcb..f280b3d4018a 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -5,8 +5,8 @@ import { Hub, makeMain, runWithAsyncContext } from '@sentry/core'; import type { fetch as FetchType } from 'undici'; import { NodeClient } from '../../src/client'; -import type { UndiciOptions } from '../../src/integrations/undici'; -import { Undici } from '../../src/integrations/undici'; +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'; @@ -30,7 +30,7 @@ beforeAll(async () => { const DEFAULT_OPTIONS = getDefaultNodeClientOptions({ dsn: SENTRY_DSN, tracesSampler: () => true, - integrations: [new Undici()], + integrations: [nativeNodeFetchintegration()], }); beforeEach(() => { @@ -377,6 +377,76 @@ conditionalTest({ min: 16 })('Undici integration', () => { 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); + const hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + + 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 { diff --git a/packages/node/test/onuncaughtexception.test.ts b/packages/node/test/onuncaughtexception.test.ts index 7d2544e63f91..c06a3cb43a69 100644 --- a/packages/node/test/onuncaughtexception.test.ts +++ b/packages/node/test/onuncaughtexception.test.ts @@ -1,7 +1,7 @@ import * as SentryCore from '@sentry/core'; import type { NodeClient } from '../src/client'; -import { OnUncaughtException, makeErrorHandler } from '../src/integrations/onuncaughtexception'; +import { makeErrorHandler, onUncaughtExceptionIntegration } from '../src/integrations/onuncaughtexception'; const client = { getOptions: () => ({}), @@ -19,8 +19,8 @@ jest.mock('@sentry/core', () => { describe('uncaught exceptions', () => { test('install global listener', () => { - const integration = new OnUncaughtException(); - integration.setup(client); + const integration = onUncaughtExceptionIntegration(); + integration.setup!(client); expect(process.listeners('uncaughtException')).toHaveLength(1); }); diff --git a/packages/node/test/onunhandledrejection.test.ts b/packages/node/test/onunhandledrejection.test.ts index 0667cd9570b2..b62da7c02fe0 100644 --- a/packages/node/test/onunhandledrejection.test.ts +++ b/packages/node/test/onunhandledrejection.test.ts @@ -1,7 +1,7 @@ import { Hub } from '@sentry/core'; import type { NodeClient } from '../src/client'; -import { OnUnhandledRejection, makeUnhandledPromiseHandler } from '../src/integrations/onunhandledrejection'; +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; @@ -20,8 +20,8 @@ jest.mock('@sentry/core', () => { describe('unhandled promises', () => { test('install global listener', () => { - const integration = new OnUnhandledRejection(); - integration.setup(client); + const integration = onUnhandledRejectionIntegration(); + integration.setup!(client); expect(process.listeners('unhandledRejection')).toHaveLength(1); }); diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index da1e794690de..eb5adacf7baf 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -74,6 +74,17 @@ export { // eslint-disable-next-line deprecation/deprecation deepReadDirSync, Integrations, + consoleIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + modulesIntegration, + contextLinesIntegration, + nodeContextIntegration, + localVariablesIntegration, + requestDataIntegration, + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, Handlers, setMeasurement, getActiveSpan, @@ -88,11 +99,7 @@ export { // eslint-disable-next-line deprecation/deprecation getModuleFromFilename, createGetModuleFromFilename, - functionToStringIntegration, hapiErrorPlugin, - inboundFiltersIntegration, - linkedErrorsIntegration, - requestDataIntegration, runWithAsyncContext, // eslint-disable-next-line deprecation/deprecation enableAnrDetection, diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index abc135a6b750..84504f7566a2 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -72,6 +72,7 @@ export { // eslint-disable-next-line deprecation/deprecation deepReadDirSync, Handlers, + // eslint-disable-next-line deprecation/deprecation Integrations, setMeasurement, getActiveSpan, @@ -93,4 +94,16 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, runWithAsyncContext, + consoleIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + modulesIntegration, + contextLinesIntegration, + nodeContextIntegration, + localVariablesIntegration, + anrIntegration, + hapiIntegration, + httpIntegration, + nativeNodeFetchintegration, + spotlightIntegration, } from '@sentry/node'; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 32fcec426df4..c01311520695 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -68,6 +68,17 @@ export { // eslint-disable-next-line deprecation/deprecation deepReadDirSync, Integrations, + consoleIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + modulesIntegration, + contextLinesIntegration, + nodeContextIntegration, + localVariablesIntegration, + requestDataIntegration, + functionToStringIntegration, + inboundFiltersIntegration, + linkedErrorsIntegration, Handlers, setMeasurement, getActiveSpan, @@ -82,11 +93,7 @@ export { // eslint-disable-next-line deprecation/deprecation getModuleFromFilename, createGetModuleFromFilename, - functionToStringIntegration, hapiErrorPlugin, - inboundFiltersIntegration, - linkedErrorsIntegration, - requestDataIntegration, metrics, runWithAsyncContext, // eslint-disable-next-line deprecation/deprecation From c51891b06a977a5a7e80cf53fb5ed1bd230a0d8f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 11:10:14 +0100 Subject: [PATCH 10/68] test(ember): Make test more robust (#10434) Saw a flake for this, we should ignore long-task spans as they are unpredictable. --- packages/ember/tests/helpers/utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index 16a34bde340c..0be2c3d2f422 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -65,9 +65,12 @@ export function assertSentryTransactions( // instead of checking the specific order of runloop spans (which is brittle), // we check (below) that _any_ runloop spans are added + // Also we ignore ui.long-task spans, as they are brittle and may or may not appear const filteredSpans = spans - // eslint-disable-next-line deprecation/deprecation - .filter(span => !span.op?.startsWith('ui.ember.runloop.')) + .filter(span => { + const op = spanToJSON(span).op; + return !op?.startsWith('ui.ember.runloop.') && !op?.startsWith('ui.long-task'); + }) .map(s => { // eslint-disable-next-line deprecation/deprecation return `${s.op} | ${spanToJSON(s).description}`; From 6155af51fd2146fa9ef717de8a0b5dea3a6d5e18 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 12:12:36 +0100 Subject: [PATCH 11/68] ci: Streamline browser integration tests on CI (#10435) This does two things: 1. Only run webkit tests for the full bundle & esm tests (no need to run this for every variation...) 2. Shard the tests for better parallelization --- .github/workflows/build.yml | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eda494c77698..b1f974dad511 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -569,7 +569,7 @@ jobs: yarn test:integration job_browser_playwright_tests: - name: Playwright (${{ matrix.bundle }}) Tests + name: Playwright (${{ matrix.bundle }}${{ matrix.shard && format(' {0}/{1}', matrix.shard, matrix.shards) || ''}}) Tests needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04-large-js @@ -591,6 +591,36 @@ jobs: - bundle_tracing_es6_min - bundle_tracing_replay_es6 - bundle_tracing_replay_es6_min + project: + - chromium + include: + # Only check all projects for esm & full bundle + # We also shard the tests as they take the longest + - bundle: bundle_tracing_replay_es6_min + project: '' + shard: 1 + shards: 2 + - bundle: bundle_tracing_replay_es6_min + project: '' + shard: 2 + shards: 2 + - bundle: esm + project: '' + shard: 1 + shards: 3 + - bundle: esm + shard: 2 + shards: 3 + - bundle: esm + project: '' + shard: 3 + shards: 3 + exclude: + # Do not run the default chromium-only tests + - bundle: bundle_tracing_replay_es6_min + project: 'chromium' + - bundle: esm + project: 'chromium' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -628,9 +658,8 @@ jobs: - name: Run Playwright tests env: PW_BUNDLE: ${{ matrix.bundle }} - run: | - cd dev-packages/browser-integration-tests - yarn test:ci + working-directory: dev-packages/browser-integration-tests + run: yarn test:ci${{ matrix.project && format(' --project={0}', matrix.project) || '' }}${{ matrix.shard && format(' --shard={0}/{1}', matrix.shard, matrix.shards) || '' }} job_browser_loader_tests: name: Playwright Loader (${{ matrix.bundle }}) Tests From 5fd5c5dd265fecf90f302501712b25d62b058b06 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 13:03:22 +0100 Subject: [PATCH 12/68] feat(angular): Export custom `browserTracingIntegration()` (#10353) Also deprecate the routing Instrumentation. This is WIP on top of https://github.com/getsentry/sentry-javascript/pull/10351, to show how that would work. No idea how to get the tests working for this, though... --- .../test-applications/angular-17/src/main.ts | 6 +- packages/angular-ivy/README.md | 10 +-- packages/angular/README.md | 8 +- packages/angular/src/index.ts | 4 +- packages/angular/src/tracing.ts | 90 +++++++++++++++++-- packages/angular/test/tracing.test.ts | 2 + packages/angular/test/utils/index.ts | 1 + .../src/browser/browserTracingIntegration.ts | 8 +- 8 files changed, 104 insertions(+), 25 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts index 7732c602bb28..761a7329a91f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/main.ts @@ -7,11 +7,7 @@ import * as Sentry from '@sentry/angular-ivy'; Sentry.init({ dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', tracesSampleRate: 1.0, - integrations: [ - new Sentry.BrowserTracing({ - routingInstrumentation: Sentry.routingInstrumentation, - }), - ], + integrations: [Sentry.browserTracingIntegration({})], tunnel: `http://localhost:3031/`, // proxy server debug: true, }); diff --git a/packages/angular-ivy/README.md b/packages/angular-ivy/README.md index 6967e7570a82..438bb0fb5ff5 100644 --- a/packages/angular-ivy/README.md +++ b/packages/angular-ivy/README.md @@ -93,16 +93,14 @@ Registering a Trace Service is a 3-step process. instrumentation: ```javascript -import { init, instrumentAngularRouting, BrowserTracing } from '@sentry/angular-ivy'; +import { init, browserTracingIntegration } from '@sentry/angular-ivy'; init({ dsn: '__DSN__', - integrations: [ - new BrowserTracing({ - tracingOrigins: ['localhost', 'https://yourserver.io/api'], - routingInstrumentation: instrumentAngularRouting, - }), + integrations: [ + browserTracingIntegration(), ], + tracePropagationTargets: ['localhost', 'https://yourserver.io/api'], tracesSampleRate: 1, }); ``` diff --git a/packages/angular/README.md b/packages/angular/README.md index 302b060bdb39..a3e15a426196 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -93,16 +93,14 @@ Registering a Trace Service is a 3-step process. instrumentation: ```javascript -import { init, instrumentAngularRouting, BrowserTracing } from '@sentry/angular'; +import { init, browserTracingIntegration } from '@sentry/angular'; init({ dsn: '__DSN__', integrations: [ - new BrowserTracing({ - tracingOrigins: ['localhost', 'https://yourserver.io/api'], - routingInstrumentation: instrumentAngularRouting, - }), + browserTracingIntegration(), ], + tracePropagationTargets: ['localhost', 'https://yourserver.io/api'], tracesSampleRate: 1, }); ``` diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index f7f0536463a2..a2b1195c4e3c 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -7,9 +7,11 @@ export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { // eslint-disable-next-line deprecation/deprecation getActiveTransaction, - // TODO `instrumentAngularRouting` is just an alias for `routingInstrumentation`; deprecate the latter at some point + // eslint-disable-next-line deprecation/deprecation instrumentAngularRouting, // new name + // eslint-disable-next-line deprecation/deprecation routingInstrumentation, // legacy name + browserTracingIntegration, TraceClassDecorator, TraceMethodDecorator, TraceDirective, diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index efd2c840420b..5b2f74615c45 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -7,9 +7,21 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { NavigationCancel, NavigationError, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; -import { WINDOW, getCurrentScope } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; -import type { Span, Transaction, TransactionContext } from '@sentry/types'; +import { + WINDOW, + browserTracingIntegration as originalBrowserTracingIntegration, + getCurrentScope, + startBrowserTracingNavigationSpan, +} from '@sentry/browser'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + getActiveSpan, + getClient, + spanToJSON, + startInactiveSpan, +} from '@sentry/core'; +import type { Integration, Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; import { Subscription } from 'rxjs'; @@ -23,8 +35,12 @@ let instrumentationInitialized: boolean; let stashedStartTransaction: (context: TransactionContext) => Transaction | undefined; let stashedStartTransactionOnLocationChange: boolean; +let hooksBasedInstrumentation = false; + /** * Creates routing instrumentation for Angular Router. + * + * @deprecated Use `browserTracingIntegration()` instead, which includes Angular-specific instrumentation out of the box. */ export function routingInstrumentation( customStartTransaction: (context: TransactionContext) => Transaction | undefined, @@ -47,8 +63,35 @@ export function routingInstrumentation( } } +/** + * Creates routing instrumentation for Angular Router. + * + * @deprecated Use `browserTracingIntegration()` instead, which includes Angular-specific instrumentation out of the box. + */ +// eslint-disable-next-line deprecation/deprecation export const instrumentAngularRouting = routingInstrumentation; +/** + * A custom BrowserTracing integration for Angular. + * + * Use this integration in combination with `TraceService` + */ +export function browserTracingIntegration( + options: Parameters[0] = {}, +): Integration { + // If the user opts out to set this up, we just don't initialize this. + // That way, the TraceService will not actually do anything, functionally disabling this. + if (options.instrumentNavigation !== false) { + instrumentationInitialized = true; + hooksBasedInstrumentation = true; + } + + return originalBrowserTracingIntegration({ + ...options, + instrumentNavigation: false, + }); +} + /** * Grabs active transaction off scope. * @@ -74,7 +117,44 @@ export class TraceService implements OnDestroy { return; } + if (this._routingSpan) { + this._routingSpan.end(); + this._routingSpan = null; + } + + const client = getClient(); const strippedUrl = stripUrlQueryAndFragment(navigationEvent.url); + + if (client && hooksBasedInstrumentation) { + if (!getActiveSpan()) { + startBrowserTracingNavigationSpan(client, { + name: strippedUrl, + op: 'navigation', + origin: 'auto.navigation.angular', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }); + } + + // eslint-disable-next-line deprecation/deprecation + this._routingSpan = + startInactiveSpan({ + name: `${navigationEvent.url}`, + op: ANGULAR_ROUTING_OP, + origin: 'auto.ui.angular', + tags: { + 'routing.instrumentation': '@sentry/angular', + url: strippedUrl, + ...(navigationEvent.navigationTrigger && { + navigationTrigger: navigationEvent.navigationTrigger, + }), + }, + }) || null; + + return; + } + // eslint-disable-next-line deprecation/deprecation let activeTransaction = getActiveTransaction(); @@ -90,9 +170,6 @@ export class TraceService implements OnDestroy { } if (activeTransaction) { - if (this._routingSpan) { - this._routingSpan.end(); - } // eslint-disable-next-line deprecation/deprecation this._routingSpan = activeTransaction.startChild({ description: `${navigationEvent.url}`, @@ -132,6 +209,7 @@ export class TraceService implements OnDestroy { if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { transaction.updateName(route); transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, `auto.${spanToJSON(transaction).op}.angular`); } }), ); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index c2406f628128..31bd13473253 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -44,6 +44,7 @@ describe('Angular Tracing', () => { transaction = undefined; }); + /* eslint-disable deprecation/deprecation */ describe('instrumentAngularRouting', () => { it('should attach the transaction source on the pageload transaction', () => { const startTransaction = jest.fn(); @@ -57,6 +58,7 @@ describe('Angular Tracing', () => { }); }); }); + /* eslint-enable deprecation/deprecation */ describe('getParameterizedRouteFromSnapshot', () => { it.each([ diff --git a/packages/angular/test/utils/index.ts b/packages/angular/test/utils/index.ts index 83a416ca2a03..390d7fbe14ac 100644 --- a/packages/angular/test/utils/index.ts +++ b/packages/angular/test/utils/index.ts @@ -50,6 +50,7 @@ export class TestEnv { useTraceService?: boolean; additionalProviders?: Provider[]; }): Promise { + // eslint-disable-next-line deprecation/deprecation instrumentAngularRouting( conf.customStartTransaction || jest.fn(), conf.startTransactionOnPageLoad !== undefined ? conf.startTransactionOnPageLoad : true, diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index aaf30e7e6a63..31660eff00a7 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -336,7 +336,9 @@ export const browserTracingIntegration = ((_options: Partial Date: Wed, 31 Jan 2024 13:04:42 +0100 Subject: [PATCH 13/68] ref(vercel-edge): Replace `WinterCGFetch` with `winterCGFetchIntegration` (#10436) This has not been converted yet. --- packages/nextjs/src/index.types.ts | 1 + packages/vercel-edge/src/index.ts | 6 +- .../src/integrations/wintercg-fetch.ts | 154 +++++++++++------- packages/vercel-edge/src/sdk.ts | 23 ++- .../vercel-edge/test/wintercg-fetch.test.ts | 83 ++++++---- 5 files changed, 159 insertions(+), 108 deletions(-) diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 21d51a01ee56..2328208e28c5 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -23,6 +23,7 @@ export declare function init( // eslint-disable-next-line deprecation/deprecation export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations & + // eslint-disable-next-line deprecation/deprecation typeof edgeSdk.Integrations; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 2ff971fde287..8937d35d38c8 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -99,12 +99,12 @@ export { import { Integrations as CoreIntegrations, RequestData } from '@sentry/core'; import { WinterCGFetch } from './integrations/wintercg-fetch'; +export { winterCGFetchIntegration } from './integrations/wintercg-fetch'; -const INTEGRATIONS = { +/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ +export const Integrations = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, WinterCGFetch, RequestData, }; - -export { INTEGRATIONS as Integrations }; diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index 5d3d662e5b4f..507a34aedab4 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -1,14 +1,34 @@ import { instrumentFetchRequest } from '@sentry-internal/tracing'; -import { addBreadcrumb, getClient, isSentryRequestUrl } from '@sentry/core'; -import type { FetchBreadcrumbData, FetchBreadcrumbHint, HandlerDataFetch, Integration, Span } from '@sentry/types'; +import { + addBreadcrumb, + convertIntegrationFnToClass, + defineIntegration, + getClient, + isSentryRequestUrl, +} from '@sentry/core'; +import type { + Client, + FetchBreadcrumbData, + FetchBreadcrumbHint, + HandlerDataFetch, + Integration, + IntegrationClass, + IntegrationFn, + Span, +} from '@sentry/types'; import { LRUMap, addFetchInstrumentationHandler, stringMatchesSomePattern } from '@sentry/utils'; +const INTEGRATION_NAME = 'WinterCGFetch'; + +const HAS_CLIENT_MAP = new WeakMap(); + export interface Options { /** * Whether breadcrumbs should be recorded for requests * Defaults to true */ breadcrumbs: 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. @@ -16,63 +36,17 @@ export interface Options { shouldCreateSpanForRequest?: (url: string) => boolean; } -/** - * Creates spans and attaches tracing headers to fetch requests on WinterCG runtimes. - */ -export class WinterCGFetch implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'WinterCGFetch'; - - /** - * @inheritDoc - */ - public name: string = WinterCGFetch.id; - - private readonly _options: Options; +const _winterCGFetch = ((options: Partial = {}) => { + const breadcrumbs = options.breadcrumbs === undefined ? true : options.breadcrumbs; + const shouldCreateSpanForRequest = options.shouldCreateSpanForRequest; - private readonly _createSpanUrlMap: LRUMap = new LRUMap(100); - private readonly _headersUrlMap: LRUMap = new LRUMap(100); + const _createSpanUrlMap = new LRUMap(100); + const _headersUrlMap = new LRUMap(100); - public constructor(_options: Partial = {}) { - this._options = { - breadcrumbs: _options.breadcrumbs === undefined ? true : _options.breadcrumbs, - shouldCreateSpanForRequest: _options.shouldCreateSpanForRequest, - }; - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - const spans: Record = {}; - - addFetchInstrumentationHandler(handlerData => { - if (!getClient()?.getIntegrationByName?.('WinterCGFetch')) { - return; - } - - if (isSentryRequestUrl(handlerData.fetchData.url, getClient())) { - return; - } - - instrumentFetchRequest( - handlerData, - this._shouldCreateSpan.bind(this), - this._shouldAttachTraceData.bind(this), - spans, - 'auto.http.wintercg_fetch', - ); - - if (this._options.breadcrumbs) { - createBreadcrumb(handlerData); - } - }); - } + const spans: Record = {}; /** Decides whether to attach trace data to the outgoing fetch request */ - private _shouldAttachTraceData(url: string): boolean { + function _shouldAttachTraceData(url: string): boolean { const client = getClient(); if (!client) { @@ -85,32 +59,86 @@ export class WinterCGFetch implements Integration { return true; } - const cachedDecision = this._headersUrlMap.get(url); + const cachedDecision = _headersUrlMap.get(url); if (cachedDecision !== undefined) { return cachedDecision; } const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - this._headersUrlMap.set(url, decision); + _headersUrlMap.set(url, decision); return decision; } /** Helper that wraps shouldCreateSpanForRequest option */ - private _shouldCreateSpan(url: string): boolean { - if (this._options.shouldCreateSpanForRequest === undefined) { + function _shouldCreateSpan(url: string): boolean { + if (shouldCreateSpanForRequest === undefined) { return true; } - const cachedDecision = this._createSpanUrlMap.get(url); + const cachedDecision = _createSpanUrlMap.get(url); if (cachedDecision !== undefined) { return cachedDecision; } - const decision = this._options.shouldCreateSpanForRequest(url); - this._createSpanUrlMap.set(url, decision); + const decision = shouldCreateSpanForRequest(url); + _createSpanUrlMap.set(url, decision); return decision; } -} + + return { + name: INTEGRATION_NAME, + // TODO v8: Remove this again + // eslint-disable-next-line @typescript-eslint/no-empty-function + setupOnce() { + addFetchInstrumentationHandler(handlerData => { + const client = getClient(); + if (!client || !HAS_CLIENT_MAP.get(client)) { + return; + } + + if (isSentryRequestUrl(handlerData.fetchData.url, client)) { + return; + } + + instrumentFetchRequest( + handlerData, + _shouldCreateSpan, + _shouldAttachTraceData, + spans, + 'auto.http.wintercg_fetch', + ); + + if (breadcrumbs) { + createBreadcrumb(handlerData); + } + }); + }, + setup(client) { + HAS_CLIENT_MAP.set(client, true); + }, + }; +}) satisfies IntegrationFn; + +export const winterCGFetchIntegration = defineIntegration(_winterCGFetch); + +/** + * Creates spans and attaches tracing headers to fetch requests on WinterCG runtimes. + * + * @deprecated Use `winterCGFetchIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const WinterCGFetch = convertIntegrationFnToClass( + INTEGRATION_NAME, + winterCGFetchIntegration, +) as IntegrationClass void }> & { + new (options?: { + breadcrumbs: boolean; + shouldCreateSpanForRequest?: (url: string) => boolean; + }): Integration; +}; + +// eslint-disable-next-line deprecation/deprecation +export type WinterCGFetch = typeof WinterCGFetch; function createBreadcrumb(handlerData: HandlerDataFetch): void { const { startTimestamp, endTimestamp } = handlerData; diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index ae263b7b01b7..fe806ccfc282 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -1,17 +1,17 @@ import { - FunctionToString, - InboundFilters, - LinkedErrors, - RequestData, + functionToStringIntegration, getIntegrationsToSetup, + inboundFiltersIntegration, initAndBind, + linkedErrorsIntegration, + requestDataIntegration, } from '@sentry/core'; import type { Integration, Options } from '@sentry/types'; import { GLOBAL_OBJ, createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import { VercelEdgeClient } from './client'; -import { WinterCGFetch } from './integrations/wintercg-fetch'; +import { winterCGFetchIntegration } from './integrations/wintercg-fetch'; import { makeEdgeTransport } from './transports'; import type { VercelEdgeClientOptions, VercelEdgeOptions } from './types'; import { getVercelEnv } from './utils/vercel'; @@ -24,12 +24,10 @@ const nodeStackParser = createStackParser(nodeStackLineParser()); /** @deprecated Use `getDefaultIntegrations(options)` instead. */ export const defaultIntegrations = [ - /* eslint-disable deprecation/deprecation */ - new InboundFilters(), - new FunctionToString(), - new LinkedErrors(), - /* eslint-enable deprecation/deprecation */ - new WinterCGFetch(), + inboundFiltersIntegration(), + functionToStringIntegration(), + linkedErrorsIntegration(), + winterCGFetchIntegration(), ]; /** Get the default integrations for the browser SDK. */ @@ -37,8 +35,7 @@ export function getDefaultIntegrations(options: Options): Integration[] { return [ // eslint-disable-next-line deprecation/deprecation ...defaultIntegrations, - // eslint-disable-next-line deprecation/deprecation - ...(options.sendDefaultPii ? [new RequestData()] : []), + ...(options.sendDefaultPii ? [requestDataIntegration()] : []), ]; } diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts index e690e6785c79..2121f037e479 100644 --- a/packages/vercel-edge/test/wintercg-fetch.test.ts +++ b/packages/vercel-edge/test/wintercg-fetch.test.ts @@ -5,11 +5,11 @@ import * as sentryUtils from '@sentry/utils'; import { createStackParser } from '@sentry/utils'; import { VercelEdgeClient } from '../src/index'; -import { WinterCGFetch } from '../src/integrations/wintercg-fetch'; +import { winterCGFetchIntegration } from '../src/integrations/wintercg-fetch'; class FakeClient extends VercelEdgeClient { public getIntegrationByName(name: string): T | undefined { - return name === 'WinterCGFetch' ? (new WinterCGFetch() as unknown as T) : undefined; + return name === 'WinterCGFetch' ? (winterCGFetchIntegration() as Integration as T) : undefined; } } @@ -17,31 +17,34 @@ const addFetchInstrumentationHandlerSpy = jest.spyOn(sentryUtils, 'addFetchInstr const instrumentFetchRequestSpy = jest.spyOn(internalTracing, 'instrumentFetchRequest'); const addBreadcrumbSpy = jest.spyOn(sentryCore, 'addBreadcrumb'); -beforeEach(() => { - jest.clearAllMocks(); - - const client = new FakeClient({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - enableTracing: true, - tracesSampleRate: 1, - integrations: [], - transport: () => ({ - send: () => Promise.resolve(undefined), - flush: () => Promise.resolve(true), - }), - tracePropagationTargets: ['http://my-website.com/'], - stackParser: createStackParser(), - }); +describe('WinterCGFetch instrumentation', () => { + let client: FakeClient; + + beforeEach(() => { + jest.clearAllMocks(); + + client = new FakeClient({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + enableTracing: true, + tracesSampleRate: 1, + integrations: [], + transport: () => ({ + send: () => Promise.resolve(undefined), + flush: () => Promise.resolve(true), + }), + tracePropagationTargets: ['http://my-website.com/'], + stackParser: createStackParser(), + }); - jest.spyOn(sentryCore, 'getClient').mockImplementation(() => client); -}); + jest.spyOn(sentryCore, 'getClient').mockImplementation(() => client); + }); -describe('WinterCGFetch instrumentation', () => { it('should call `instrumentFetchRequest` for outgoing fetch requests', () => { - const integration = new WinterCGFetch(); addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); + const integration = winterCGFetchIntegration(); integration.setupOnce(); + integration.setup!(client); const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]; expect(fetchInstrumentationHandlerCallback).toBeDefined(); @@ -70,11 +73,32 @@ describe('WinterCGFetch instrumentation', () => { expect(shouldCreateSpan('https://www.3rd-party-website.at/')).toBe(true); }); + it('should not instrument if client is not setup', () => { + addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); + + const integration = winterCGFetchIntegration(); + integration.setupOnce(); + // integration.setup!(client) is not called! + + const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]; + expect(fetchInstrumentationHandlerCallback).toBeDefined(); + + const startHandlerData: HandlerDataFetch = { + fetchData: { url: 'http://my-website.com/', method: 'POST' }, + args: ['http://my-website.com/'], + startTimestamp: Date.now(), + }; + fetchInstrumentationHandlerCallback(startHandlerData); + + expect(instrumentFetchRequestSpy).not.toHaveBeenCalled(); + }); + it('should call `instrumentFetchRequest` for outgoing fetch requests to Sentry', () => { - const integration = new WinterCGFetch(); addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); + const integration = winterCGFetchIntegration(); integration.setupOnce(); + integration.setup!(client); const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]; expect(fetchInstrumentationHandlerCallback).toBeDefined(); @@ -90,14 +114,15 @@ describe('WinterCGFetch instrumentation', () => { }); it('should properly apply the `shouldCreateSpanForRequest` option', () => { - const integration = new WinterCGFetch({ + addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); + + const integration = winterCGFetchIntegration({ shouldCreateSpanForRequest(url) { return url === 'http://only-acceptable-url.com/'; }, }); - addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); - integration.setupOnce(); + integration.setup!(client); const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]; expect(fetchInstrumentationHandlerCallback).toBeDefined(); @@ -117,10 +142,11 @@ describe('WinterCGFetch instrumentation', () => { }); it('should create a breadcrumb for an outgoing request', () => { - const integration = new WinterCGFetch(); addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); + const integration = winterCGFetchIntegration(); integration.setupOnce(); + integration.setup!(client); const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]; expect(fetchInstrumentationHandlerCallback).toBeDefined(); @@ -153,12 +179,11 @@ describe('WinterCGFetch instrumentation', () => { }); it('should not create a breadcrumb for an outgoing request if `breadcrumbs: false` is set', () => { - const integration = new WinterCGFetch({ - breadcrumbs: false, - }); addFetchInstrumentationHandlerSpy.mockImplementationOnce(() => undefined); + const integration = winterCGFetchIntegration({ breadcrumbs: false }); integration.setupOnce(); + integration.setup!(client); const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]; expect(fetchInstrumentationHandlerCallback).toBeDefined(); From 3e148e6ccd8bf79d815ea64dcaadabe96adb2a7b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 14:04:52 +0100 Subject: [PATCH 14/68] feat(vue): Deprecate `new VueIntegration()` (#10440) `vueIntegration()` is already exported, but we didn't deprecate the class yet. --- packages/vue/src/index.ts | 6 +++++- packages/vue/src/integration.ts | 2 ++ packages/vue/test/integration/VueIntegration.test.ts | 4 ++-- packages/vue/test/integration/init.test.ts | 3 +-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index d89359530043..030324af9430 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -4,4 +4,8 @@ export { init } from './sdk'; export { vueRouterInstrumentation } from './router'; export { attachErrorHandler } from './errorhandler'; export { createTracingMixins } from './tracing'; -export { vueIntegration, VueIntegration } from './integration'; +export { + vueIntegration, + // eslint-disable-next-line deprecation/deprecation + VueIntegration, +} from './integration'; diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index 5065e1486400..8150ea6b95d6 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -35,6 +35,8 @@ export const vueIntegration = defineIntegration(_vueIntegration); /** * Initialize Vue error & performance tracking. + * + * @deprecated Use `vueIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const VueIntegration = convertIntegrationFnToClass( diff --git a/packages/vue/test/integration/VueIntegration.test.ts b/packages/vue/test/integration/VueIntegration.test.ts index 08af038676d0..aeea0ebf1451 100644 --- a/packages/vue/test/integration/VueIntegration.test.ts +++ b/packages/vue/test/integration/VueIntegration.test.ts @@ -36,7 +36,7 @@ describe('Sentry.VueIntegration', () => { }); // This would normally happen through client.addIntegration() - const integration = new Sentry.VueIntegration({ app }); + const integration = Sentry.vueIntegration({ app }); integration['setup']?.(Sentry.getClient() as Client); app.mount(el); @@ -58,7 +58,7 @@ describe('Sentry.VueIntegration', () => { app.mount(el); // This would normally happen through client.addIntegration() - const integration = new Sentry.VueIntegration({ app }); + const integration = Sentry.vueIntegration({ app }); integration['setup']?.(Sentry.getClient() as Client); expect(warnings).toEqual([ diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index 6a117427e2c8..c0652ad37485 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -1,6 +1,5 @@ import { createApp } from 'vue'; -import { VueIntegration } from '../../src/integration'; import type { Options } from '../../src/types'; import * as Sentry from './../../src'; @@ -104,7 +103,7 @@ Update your \`Sentry.init\` call with an appropriate config option: }); function runInit(options: Partial): void { - const integration = new VueIntegration(); + const integration = Sentry.vueIntegration(); Sentry.init({ dsn: PUBLIC_DSN, From 793ad7193cde285c8a6fb008f73ef5b1599e423c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 14:05:00 +0100 Subject: [PATCH 15/68] feat(bun): Export `bunServerIntegration()` (#10439) And deprecate `new BunServer()` --- packages/bun/src/index.ts | 6 +++--- packages/bun/src/integrations/bunserver.ts | 7 ++++++- packages/bun/src/integrations/index.ts | 1 - packages/bun/src/sdk.ts | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) delete mode 100644 packages/bun/src/integrations/index.ts diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d793a1a93551..ffe316fd30ec 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -128,15 +128,15 @@ export { import { Integrations as CoreIntegrations } from '@sentry/core'; import { Integrations as NodeIntegrations } from '@sentry/node'; - -import * as BunIntegrations from './integrations'; +import { BunServer } from './integrations/bunserver'; +export { bunServerIntegration } from './integrations/bunserver'; const INTEGRATIONS = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, // eslint-disable-next-line deprecation/deprecation ...NodeIntegrations, - ...BunIntegrations, + BunServer, }; export { INTEGRATIONS as Integrations }; diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index b1dc4c6892e0..e262cd4e70a4 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -5,6 +5,7 @@ import { captureException, continueTrace, convertIntegrationFnToClass, + defineIntegration, getCurrentScope, runWithAsyncContext, setHttpStatus, @@ -15,7 +16,7 @@ import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; const INTEGRATION_NAME = 'BunServer'; -const bunServerIntegration = (() => { +const _bunServerIntegration = (() => { return { name: INTEGRATION_NAME, setupOnce() { @@ -24,8 +25,12 @@ const bunServerIntegration = (() => { }; }) satisfies IntegrationFn; +export const bunServerIntegration = defineIntegration(_bunServerIntegration); + /** * Instruments `Bun.serve` to automatically create transactions and capture errors. + * + * @deprecated Use `bunServerIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const BunServer = convertIntegrationFnToClass(INTEGRATION_NAME, bunServerIntegration); diff --git a/packages/bun/src/integrations/index.ts b/packages/bun/src/integrations/index.ts deleted file mode 100644 index 95d17cf80e66..000000000000 --- a/packages/bun/src/integrations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BunServer } from './bunserver'; diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index 74150b72ef6f..f8dbcb99c0df 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -17,7 +17,7 @@ import { import type { Integration, Options } from '@sentry/types'; import { BunClient } from './client'; -import { BunServer } from './integrations'; +import { bunServerIntegration } from './integrations/bunserver'; import { makeFetchTransport } from './transports'; import type { BunOptions } from './types'; @@ -41,7 +41,7 @@ export const defaultIntegrations = [ nodeContextIntegration(), modulesIntegration(), // Bun Specific - new BunServer(), + bunServerIntegration(), ]; /** Get the default integrations for the Bun SDK. */ From eadf9c0575a686dd3161065f5dd8a8608fd19f74 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 14:05:20 +0100 Subject: [PATCH 16/68] feat(browser): Export `browserProfilingIntegration` (#10438) And deprecate `new BrowserProfilingIntegration()` --- packages/browser/src/index.ts | 6 +++++- packages/browser/src/profiling/integration.ts | 10 ++++++++-- .../browser/test/unit/profiling/integration.test.ts | 3 +-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 408a64081a02..cda5f65a800e 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -83,4 +83,8 @@ export type { SpanStatusType } from '@sentry/core'; export type { Span } from '@sentry/types'; export { makeBrowserOfflineTransport } from './transports/offline'; export { onProfilingStartRouteTransaction } from './profiling/hubextensions'; -export { BrowserProfilingIntegration } from './profiling/integration'; +export { + // eslint-disable-next-line deprecation/deprecation + BrowserProfilingIntegration, + browserProfilingIntegration, +} from './profiling/integration'; diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index a3af7744c4e4..bf8e56a626b5 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass, getCurrentScope } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core'; import type { Client, EventEnvelope, Integration, IntegrationClass, IntegrationFn, Transaction } from '@sentry/types'; import type { Profile } from '@sentry/types/src/profiling'; import { logger } from '@sentry/utils'; @@ -18,7 +18,7 @@ import { const INTEGRATION_NAME = 'BrowserProfiling'; -const browserProfilingIntegration = (() => { +const _browserProfilingIntegration = (() => { return { name: INTEGRATION_NAME, // TODO v8: Remove this @@ -102,6 +102,8 @@ const browserProfilingIntegration = (() => { }; }) satisfies IntegrationFn; +export const browserProfilingIntegration = defineIntegration(_browserProfilingIntegration); + /** * Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"] * This exists because we do not want to await async profiler.stop calls as transaction.finish is called @@ -110,9 +112,13 @@ const browserProfilingIntegration = (() => { * integration less reliable as we might be dropping profiles when the cache is full. * * @experimental + * @deprecated Use `browserProfilingIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const BrowserProfilingIntegration = convertIntegrationFnToClass( INTEGRATION_NAME, browserProfilingIntegration, ) as IntegrationClass void }>; + +// eslint-disable-next-line deprecation/deprecation +export type BrowserProfilingIntegration = typeof BrowserProfilingIntegration; diff --git a/packages/browser/test/unit/profiling/integration.test.ts b/packages/browser/test/unit/profiling/integration.test.ts index b69d3a52d655..9394221b0e4b 100644 --- a/packages/browser/test/unit/profiling/integration.test.ts +++ b/packages/browser/test/unit/profiling/integration.test.ts @@ -1,7 +1,6 @@ import type { BrowserClient } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; -import { BrowserProfilingIntegration } from '../../../src/profiling/integration'; import type { JSSelfProfile } from '../../../src/profiling/jsSelfProfiling'; describe('BrowserProfilingIntegration', () => { @@ -44,7 +43,7 @@ describe('BrowserProfilingIntegration', () => { send, }; }, - integrations: [new Sentry.BrowserTracing(), new BrowserProfilingIntegration()], + integrations: [Sentry.browserTracingIntegration(), Sentry.browserProfilingIntegration()], }); const client = Sentry.getClient(); From 369faf4c57323d326662730975ba8676f68cfdad Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 31 Jan 2024 14:11:46 +0100 Subject: [PATCH 17/68] feat(core): Pass `name` & `attributes` to `tracesSampler` (#10426) Updates `tracesSampler` to receive `name` and `attributes` instead of relying on `transactionContext` being passed. --------- Co-authored-by: Luca Forstner --- MIGRATION.md | 7 +++++ packages/core/src/tracing/hubextensions.ts | 12 +++++++ packages/core/test/lib/tracing/trace.test.ts | 30 ++++++++++++++++++ .../opentelemetry-node/src/spanprocessor.ts | 1 + packages/opentelemetry/src/sampler.ts | 8 +++-- packages/opentelemetry/src/spanExporter.ts | 6 ++-- packages/opentelemetry/test/trace.test.ts | 31 ++++++++++++++----- 7 files changed, 82 insertions(+), 13 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 7dc88f4b78b1..d52fd21f07fc 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -10,6 +10,13 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecated `transactionContext` passed to `tracesSampler` + +Instead of an `transactionContext` being passed to the `tracesSampler` callback, the callback will directly receive +`name` and `attributes` going forward. You can use these to make your sampling decisions, while `transactionContext` +will be removed in v8. Note that the `attributes` are only the attributes at span creation time, and some attributes may +only be set later during the span lifecycle (and thus not be available during sampling). + ## Deprecate using `getClient()` to check if the SDK was initialized In v8, `getClient()` will stop returning `undefined` if `Sentry.init()` was not called. For cases where this may be used diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index e35543f16631..51748ea72351 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -65,8 +65,14 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru // eslint-disable-next-line deprecation/deprecation let transaction = new Transaction(transactionContext, this); transaction = sampleTransaction(transaction, options, { + name: transactionContext.name, parentSampled: transactionContext.parentSampled, transactionContext, + attributes: { + // eslint-disable-next-line deprecation/deprecation + ...transactionContext.data, + ...transactionContext.attributes, + }, ...customSamplingContext, }); if (transaction.isRecording()) { @@ -106,8 +112,14 @@ export function startIdleTransaction( delayAutoFinishUntilSignal, ); transaction = sampleTransaction(transaction, options, { + name: transactionContext.name, parentSampled: transactionContext.parentSampled, transactionContext, + attributes: { + // eslint-disable-next-line deprecation/deprecation + ...transactionContext.data, + ...transactionContext.attributes, + }, ...customSamplingContext, }); if (transaction.isRecording()) { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index fca086f10c94..7cb09e1569ec 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -4,6 +4,7 @@ import { addTracingExtensions, getCurrentScope, makeMain, + setCurrentClient, spanToJSON, withScope, } from '../../../src'; @@ -357,6 +358,35 @@ describe('startSpan', () => { expect(span).toBeDefined(); }); }); + + it('samples with a tracesSampler', () => { + const tracesSampler = jest.fn(() => { + return true; + }); + + const options = getDefaultTestClientOptions({ tracesSampler }); + client = new TestClient(options); + setCurrentClient(client); + + startSpan( + { name: 'outer', attributes: { test1: 'aa', test2: 'aa' }, data: { test1: 'bb', test3: 'bb' } }, + outerSpan => { + expect(outerSpan).toBeDefined(); + }, + ); + + expect(tracesSampler).toBeCalledTimes(1); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: undefined, + name: 'outer', + attributes: { + test1: 'aa', + test2: 'aa', + test3: 'bb', + }, + transactionContext: expect.objectContaining({ name: 'outer', parentSampled: undefined }), + }); + }); }); describe('startSpanManual', () => { diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 1ca4e3584b1f..d14d77010422 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -79,6 +79,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { const transaction = getCurrentHub().startTransaction({ name: otelSpan.name, ...traceCtx, + attributes: otelSpan.attributes, instrumenter: 'otel', startTimestamp: convertOtelTimeToSeconds(otelSpan.startTime), spanId: otelSpanId, diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index a3f2d07eddd4..0cd15ebb2daf 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -3,7 +3,7 @@ import type { Attributes, Context, SpanContext } from '@opentelemetry/api'; import { TraceFlags, isSpanContextValid, trace } from '@opentelemetry/api'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; -import { hasTracingEnabled } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, hasTracingEnabled } from '@sentry/core'; import type { Client, ClientOptions, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; @@ -27,7 +27,7 @@ export class SentrySampler implements Sampler { traceId: string, spanName: string, _spanKind: unknown, - _attributes: unknown, + spanAttributes: unknown, _links: unknown, ): SamplingResult { const options = this._client.getOptions(); @@ -54,6 +54,8 @@ export class SentrySampler implements Sampler { } const sampleRate = getSampleRate(options, { + name: spanName, + attributes: spanAttributes, transactionContext: { name: spanName, parentSampled, @@ -62,7 +64,7 @@ export class SentrySampler implements Sampler { }); const attributes: Attributes = { - [InternalSentrySemanticAttributes.SAMPLE_RATE]: Number(sampleRate), + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: Number(sampleRate), }; if (typeof parentSampled === 'boolean') { diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 0d57d1009e31..f4bf4bc3aec4 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -3,7 +3,7 @@ import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { flush, getCurrentScope } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, flush, getCurrentScope } from '@sentry/core'; import type { Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -176,7 +176,7 @@ function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransact metadata: { dynamicSamplingContext, source, - sampleRate: span.attributes[InternalSentrySemanticAttributes.SAMPLE_RATE] as number | undefined, + sampleRate: span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as number | undefined, ...metadata, }, data: removeSentryAttributes(data), @@ -267,7 +267,7 @@ function removeSentryAttributes(data: Record): Record { span => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }); expect(getSpanMetadata(span)).toEqual(undefined); @@ -227,7 +228,7 @@ describe('trace', () => { [InternalSentrySemanticAttributes.SOURCE]: 'task', [InternalSentrySemanticAttributes.ORIGIN]: 'auto.test.origin', [InternalSentrySemanticAttributes.OP]: 'my-op', - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }); expect(getSpanMetadata(span)).toEqual({ requestPath: 'test-path' }); @@ -253,7 +254,7 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanName(span)).toEqual('outer'); expect(getSpanAttributes(span)).toEqual({ - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, test1: 'test 1', test2: 2, }); @@ -326,7 +327,7 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }); expect(getSpanMetadata(span)).toEqual(undefined); @@ -341,7 +342,7 @@ describe('trace', () => { expect(span2).toBeDefined(); expect(getSpanAttributes(span2)).toEqual({ - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [InternalSentrySemanticAttributes.SOURCE]: 'task', [InternalSentrySemanticAttributes.ORIGIN]: 'auto.test.origin', [InternalSentrySemanticAttributes.OP]: 'my-op', @@ -366,7 +367,7 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanName(span)).toEqual('outer'); expect(getSpanAttributes(span)).toEqual({ - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, test1: 'test 1', test2: 2, }); @@ -451,7 +452,7 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanName(span)).toEqual('outer'); expect(getSpanAttributes(span)).toEqual({ - [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, test1: 'test 1', test2: 2, }); @@ -688,6 +689,10 @@ describe('trace (sampling)', () => { expect(tracesSampler).toBeCalledTimes(1); expect(tracesSampler).toHaveBeenLastCalledWith({ parentSampled: undefined, + name: 'outer', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, transactionContext: { name: 'outer', parentSampled: undefined }, }); @@ -705,6 +710,8 @@ describe('trace (sampling)', () => { expect(tracesSampler).toHaveBeenCalledTimes(3); expect(tracesSampler).toHaveBeenLastCalledWith({ parentSampled: false, + name: 'inner2', + attributes: {}, transactionContext: { name: 'inner2', parentSampled: false }, }); }); @@ -727,6 +734,10 @@ describe('trace (sampling)', () => { expect(tracesSampler).toBeCalledTimes(1); expect(tracesSampler).toHaveBeenLastCalledWith({ parentSampled: undefined, + name: 'outer', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, transactionContext: { name: 'outer', parentSampled: undefined }, }); @@ -744,6 +755,8 @@ describe('trace (sampling)', () => { expect(tracesSampler).toHaveBeenCalledTimes(3); expect(tracesSampler).toHaveBeenLastCalledWith({ parentSampled: false, + name: 'inner2', + attributes: {}, transactionContext: { name: 'inner2', parentSampled: false }, }); @@ -757,6 +770,8 @@ describe('trace (sampling)', () => { expect(tracesSampler).toHaveBeenCalledTimes(4); expect(tracesSampler).toHaveBeenLastCalledWith({ parentSampled: undefined, + name: 'outer3', + attributes: {}, transactionContext: { name: 'outer3', parentSampled: undefined }, }); }); @@ -799,6 +814,8 @@ describe('trace (sampling)', () => { expect(tracesSampler).toBeCalledTimes(1); expect(tracesSampler).toHaveBeenLastCalledWith({ parentSampled: true, + name: 'outer', + attributes: {}, transactionContext: { name: 'outer', parentSampled: true, From acc5e33d925768ab21e0a95ca4add7b278f09e72 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 31 Jan 2024 14:21:49 +0100 Subject: [PATCH 18/68] ci: Use larger runner for build job (#10437) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1f974dad511..036cbccd5947 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -212,7 +212,7 @@ jobs: job_build: name: Build needs: [job_get_metadata, job_install_deps] - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04-large-js timeout-minutes: 30 if: | (needs.job_get_metadata.outputs.changed_any_code == 'true' || github.event_name != 'pull_request') From f5ffacf3839a98d0d7014079c7cf1ff52e713036 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 Jan 2024 16:21:32 +0100 Subject: [PATCH 19/68] test(e2e): Add tests to SvelteKit 2.x E2E test app (#9944) Add proper e2e tests to our Sveltekit 2.x test application. 1. Distributed pageload trace - important for browserTracing rework 2. Distributed navigation trace - important for browserTracing rework 3. Client side component error 4. Client side universal load error 5. Server side universal load error 6. Server side server load error 7. Server route/API error --- .../sveltekit-2/event-proxy-server.ts | 22 +-- .../sveltekit-2/package.json | 9 +- .../sveltekit-2/playwright.config.ts | 25 ++- .../sveltekit-2/src/app.html | 2 +- .../sveltekit-2/src/hooks.server.ts | 7 +- .../sveltekit-2/src/routes/+layout.svelte | 13 ++ .../sveltekit-2/src/routes/+page.svelte | 24 +++ .../src/routes/api/users/+server.ts | 3 + .../src/routes/building/+page.svelte | 15 ++ .../src/routes/client-error/+page.svelte | 9 + .../routes/server-load-error/+page.server.ts | 6 + .../src/routes/server-load-error/+page.svelte | 9 + .../routes/server-route-error/+page.svelte | 9 + .../src/routes/server-route-error/+page.ts | 7 + .../src/routes/server-route-error/+server.ts | 6 + .../routes/universal-load-error/+page.svelte | 17 ++ .../src/routes/universal-load-error/+page.ts | 8 + .../routes/universal-load-fetch/+page.svelte | 14 ++ .../src/routes/universal-load-fetch/+page.ts | 5 + .../src/routes/users/+page.server.ts | 5 + .../sveltekit-2/src/routes/users/+page.svelte | 10 + .../src/routes/users/[id]/+page.server.ts | 5 + .../src/routes/users/[id]/+page.svelte | 14 ++ .../sveltekit-2/test/errors.client.test.ts | 56 ++++++ .../sveltekit-2/test/errors.server.test.ts | 68 +++++++ .../sveltekit-2/test/performance.test.ts | 187 ++++++++++++++++++ .../sveltekit-2/test/transaction.test.ts | 48 ----- .../sveltekit-2/test/utils.ts | 49 +++++ 28 files changed, 574 insertions(+), 78 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/client-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts 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 index 66a9e744846e..4c2df32399f0 100644 --- 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 @@ -6,7 +6,7 @@ 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 type { Envelope, EnvelopeItem, SerializedEvent } from '@sentry/types'; import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); @@ -210,13 +210,13 @@ export function waitForEnvelopeItem( export function waitForError( proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { + callback: (transactionEvent: SerializedEvent) => 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); + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); return true; } return false; @@ -226,13 +226,13 @@ export function waitForError( export function waitForTransaction( proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { + callback: (transactionEvent: SerializedEvent) => 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); + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); return true; } return false; @@ -247,7 +247,7 @@ async function registerCallbackServerPort(serverName: string, port: string): Pro await writeFile(tmpFilePath, port, { encoding: 'utf8' }); } -async function retrieveCallbackServerPort(serverName: string): Promise { +function retrieveCallbackServerPort(serverName: string): Promise { const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return await readFile(tmpFilePath, 'utf8'); + 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 b55d9ff74df6..a323c3c415be 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -10,20 +10,19 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm -v" + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod" }, "dependencies": { "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@playwright/test": "^1.27.1", + "@playwright/test": "^1.36.2", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.5.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.2.8", "svelte-check": "^3.6.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts index bfa29df7d549..5e028dc2e29a 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts @@ -1,13 +1,19 @@ 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 testEnv = process.env.TEST_ENV; if (!testEnv) { throw new Error('No test env defined'); } -const port = 3030; +const svelteKitPort = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -24,7 +30,8 @@ const config: PlaywrightTestConfig = { timeout: 10000, }, /* Run tests in files in parallel */ - fullyParallel: true, + fullyParallel: false, + workers: 1, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* `next dev` is incredibly buggy with the app dir */ @@ -36,7 +43,7 @@ const config: PlaywrightTestConfig = { /* 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:${port}`, + baseURL: `http://localhost:${svelteKitPort}`, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -55,15 +62,17 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: [ { - command: 'pnpm ts-node --esm start-event-proxy.ts', - port: 3031, + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + reuseExistingServer: false, }, { command: testEnv === 'development' - ? `pnpm wait-port ${port} && pnpm dev --port ${port}` - : `pnpm wait-port ${port} && pnpm preview --port ${port}`, - port, + ? `pnpm wait-port ${eventProxyPort} && pnpm dev --port ${svelteKitPort}` + : `pnpm wait-port ${eventProxyPort} && PORT=${svelteKitPort} node build`, + port: svelteKitPort, + reuseExistingServer: false, }, ], }; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html index 117bd026151a..435cf39f2268 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts index ae99e0e0e7b4..2a2abbb870dd 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts @@ -9,10 +9,7 @@ Sentry.init({ tracesSampleRate: 1.0, }); -const myErrorHandler = ({ error, event }: any) => { - console.error('An error occurred on the server side:', error, event); -}; - -export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); +// not logging anything to console to avoid noise in the test output +export const handleError = Sentry.handleErrorWithSentry(() => {}); export const handle = Sentry.sentryHandle(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte new file mode 100644 index 000000000000..08c4b6468a93 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + +

Sveltekit E2E Test app

+
+ +
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 5982b0ae37dd..aeb12d58603f 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 @@ -1,2 +1,26 @@

Welcome to SvelteKit

Visit kit.svelte.dev to read the documentation

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts new file mode 100644 index 000000000000..d0e4371c594b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts @@ -0,0 +1,3 @@ +export const GET = () => { + return new Response(JSON.stringify({ users: ['alice', 'bob', 'carol'] })); +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte index fde274c60705..b27edb70053d 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte @@ -1,3 +1,18 @@ + +

Check Build

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

Client error

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts new file mode 100644 index 000000000000..17dd53fb5bbb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts @@ -0,0 +1,6 @@ +export const load = async () => { + throw new Error('Server Load Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte new file mode 100644 index 000000000000..3a0942971d06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server load error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte new file mode 100644 index 000000000000..3d682e7e3462 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server Route error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts new file mode 100644 index 000000000000..298240827714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts @@ -0,0 +1,7 @@ +export const load = async ({ fetch }) => { + const res = await fetch('/server-route-error'); + const data = await res.json(); + return { + msg: data, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts new file mode 100644 index 000000000000..f1a4b94b7706 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts @@ -0,0 +1,6 @@ +export const GET = async () => { + throw new Error('Server Route Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte new file mode 100644 index 000000000000..dc2d311a0ece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte @@ -0,0 +1,17 @@ + + +

Universal load error

+ +

+ To trigger from client: Load on another route, then navigate to this route. +

+ +

+ To trigger from server: Load on this route +

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts new file mode 100644 index 000000000000..3d72bf4a890f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts @@ -0,0 +1,8 @@ +import { browser } from '$app/environment'; + +export const load = async () => { + throw new Error(`Universal Load Error (${browser ? 'browser' : 'server'})`); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte new file mode 100644 index 000000000000..563c51e8c850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte @@ -0,0 +1,14 @@ + + +

Fetching in universal load

+ +

Here's a list of a few users:

+ +
    + {#each data.users as user} +
  • {user}
  • + {/each} +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts new file mode 100644 index 000000000000..63c1ee68e1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts @@ -0,0 +1,5 @@ +export const load = async ({ fetch }) => { + const usersRes = await fetch('/api/users'); + const data = await usersRes.json(); + return { users: data.users }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts new file mode 100644 index 000000000000..a34c5450f682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async () => { + return { + msg: 'Hi everyone!', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte new file mode 100644 index 000000000000..aa804a4518fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte @@ -0,0 +1,10 @@ + +

+ All Users: +

+ +

+ message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts new file mode 100644 index 000000000000..9388f3927018 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async ({ params }) => { + return { + msg: `This is a special message for user ${params.id}`, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte new file mode 100644 index 000000000000..d348a8c57dad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte @@ -0,0 +1,14 @@ + + +

Route with dynamic params

+ +

+ User id: {$page.params.id} +

+ +

+ Secret message for user: {data.msg} +

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 new file mode 100644 index 000000000000..7942b96b41b0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../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 expect(page.getByText('Client error')).toBeVisible(); + + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; + }); + + const clickPromise = page.getByText('Throw error').click(); + + const [errorEvent, _] = await Promise.all([errorEventPromise, clickPromise]); + + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: expect.stringContaining('HTMLButtonElement'), + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); + + test('captures universal load error', async ({ page }) => { + await waitForInitialPageload(page); + await page.reload(); + + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (browser)'; + }); + + // navigating triggers the error on the client + await page.getByText('Universal Load error').click(); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + const lastFrame = errorEventFrames?.[errorEventFrames?.length - 1]; + expect(lastFrame).toEqual( + expect.objectContaining({ + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); +}); 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 new file mode 100644 index 000000000000..b7966325560a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts @@ -0,0 +1,68 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test.describe('server-side errors', () => { + test('captures universal load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (server)'; + }); + + await page.goto('/universal-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load$1', + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Load Error'; + }); + + await page.goto('/server-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load$1', + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server route (GET) error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Route Error'; + }); + + await page.goto('/server-route-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + filename: expect.stringContaining('app:///_server.ts'), + function: 'GET', + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ + runtime: 'node', + transaction: 'GET /server-route-error', + }); + }); +}); 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 new file mode 100644 index 000000000000..aed2040392e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts @@ -0,0 +1,187 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../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]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + const [clientTxnEvent, serverTxnEvent, _] = await Promise.all([ + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText('User id: 123xyz')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users/[id]', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users/[id]', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + expect(clientTxnEvent.spans?.length).toBeGreaterThan(5); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + // weird but server txn is parent of client txn + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); + + 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'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === 'GET /users'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with Server Load').click(); + + const [clientTxnEvent, serverTxnEvent, _1, _2] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + expect(page.getByText('Hi everyone')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + }); + + test('record client-side universal load fetch span and trace', async ({ page }) => { + await waitForInitialPageload(page); + + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/universal-load-fetch' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + // this transaction should be created because of the fetch call + // it should also be part of the trace + const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === 'GET /api/users'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with fetch in universal load').click(); + + const [clientTxnEvent, serverTxnEvent, _1, _2] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + expect(page.getByText('alice')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/universal-load-fetch', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /api/users', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + const clientFetchSpan = clientTxnEvent.spans?.find(s => s.op === 'http.client'); + + expect(clientFetchSpan).toMatchObject({ + description: expect.stringMatching(/^GET.*\/api\/users/), + op: 'http.client', + origin: 'auto.http.browser', + data: { + url: expect.stringContaining('/api/users'), + type: 'fetch', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'network.protocol.version': '1.1', + 'network.protocol.name': 'http', + 'http.request.redirect_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts deleted file mode 100644 index 7d621af34dcf..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect, test } from '@playwright/test'; -import axios, { AxiosError } from 'axios'; -// @ts-expect-error ok ok -import { waitForTransaction } from '../event-proxy-server.ts'; - -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 a pageload transaction', async ({ page }) => { - const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => { - return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; - }); - - await page.goto('/'); - - const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; - - 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/sveltekit-2/test/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts new file mode 100644 index 000000000000..b48b949abdd5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts @@ -0,0 +1,49 @@ +import { Page } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +/** + * Helper function that waits for the initial pageload to complete. + * + * This function + * - loads the given route ("/" by default) + * - waits for SvelteKit's hydration + * - waits for the pageload transaction to be sent (doesn't assert on it though) + * + * Useful for tests that test outcomes of _navigations_ after an initial pageload. + * Waiting on the pageload transaction excludes edge cases where navigations occur + * so quickly that the pageload idle transaction is still active. This might lead + * to cases where the routing span would be attached to the pageload transaction + * and hence eliminates a lot of flakiness. + * + */ +export async function waitForInitialPageload( + page: Page, + opts?: { route?: string; parameterizedRoute?: string; debug: boolean }, +) { + const route = opts?.route ?? '/'; + const txnName = opts?.parameterizedRoute ?? route; + const debug = opts?.debug ?? false; + + const clientPageloadTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + debug && + console.log({ + txn: txnEvent?.transaction, + op: txnEvent.contexts?.trace?.op, + trace: txnEvent.contexts?.trace?.trace_id, + span: txnEvent.contexts?.trace?.span_id, + parent: txnEvent.contexts?.trace?.parent_span_id, + }); + + return txnEvent?.transaction === txnName && txnEvent.contexts?.trace?.op === 'pageload'; + }); + + await Promise.all([ + page.goto(route), + // the test app adds the "hydrated" class to the body when hydrating + page.waitForSelector('body.hydrated'), + // also waiting for the initial pageload txn so that later navigations don't interfere + clientPageloadTxnEventPromise, + ]); + + debug && console.log('hydrated'); +} From 9fcbb84e899f6ca7c74733b0ebdeebfae75b3162 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 31 Jan 2024 16:21:56 +0100 Subject: [PATCH 20/68] test(e2e): Add tests to SvelteKit 1.x E2E test app (#9943) This PR adds proper e2e tests to our Sveltekit 1.x test application. 1. Distributed pageload trace - important for browserTracing rework 2. Distributed navigation trace - important for browserTracing rework 3. Client side component error 4. Client side universal load error 5. Server side universal load error 6. Server side server load error 7. Server route/API error --- .../sveltekit/event-proxy-server.ts | 10 +- .../test-applications/sveltekit/package.json | 9 +- .../sveltekit/playwright.config.ts | 8 +- .../test-applications/sveltekit/src/app.html | 2 +- .../sveltekit/src/hooks.server.ts | 5 +- .../sveltekit/src/routes/+layout.svelte | 10 ++ .../sveltekit/src/routes/+page.svelte | 24 ++++ .../sveltekit/src/routes/api/users/+server.ts | 3 + .../src/routes/client-error/+page.svelte | 9 ++ .../routes/server-load-error/+page.server.ts | 6 + .../src/routes/server-load-error/+page.svelte | 9 ++ .../routes/server-route-error/+page.svelte | 9 ++ .../src/routes/server-route-error/+page.ts | 7 + .../src/routes/server-route-error/+server.ts | 6 + .../routes/universal-load-error/+page.svelte | 17 +++ .../src/routes/universal-load-error/+page.ts | 8 ++ .../routes/universal-load-fetch/+page.svelte | 14 ++ .../src/routes/universal-load-fetch/+page.ts | 5 + .../src/routes/users/+page.server.ts | 5 + .../sveltekit/src/routes/users/+page.svelte | 10 ++ .../src/routes/users/[id]/+page.server.ts | 5 + .../src/routes/users/[id]/+page.svelte | 14 ++ .../sveltekit/test/errors.client.test.ts | 55 ++++++++ .../sveltekit/test/errors.server.test.ts | 71 ++++++++++ .../sveltekit/test/performance.test.ts | 124 ++++++++++++++++++ .../sveltekit/test/transaction.test.ts | 48 ------- .../test-applications/sveltekit/utils.ts | 53 ++++++++ package.json | 3 +- 28 files changed, 483 insertions(+), 66 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit/utils.ts 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 index 66a9e744846e..1bc419bd0b4c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts @@ -6,7 +6,7 @@ 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 type { Envelope, EnvelopeItem, Event, SerializedEvent } from '@sentry/types'; import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); @@ -226,13 +226,13 @@ export function waitForError( export function waitForTransaction( proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { + callback: (transactionEvent: SerializedEvent) => 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); + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); return true; } return false; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json index ad6bf6456843..ea21787939c3 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit/package.json @@ -10,20 +10,19 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm -v" + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod" }, "dependencies": { "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@playwright/test": "^1.27.1", + "@playwright/test": "^1.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-node": "^1.2.4", - "@sveltejs/kit": "^1.5.0", + "@sveltejs/kit": "^1.30.3", "svelte": "^3.54.0", "svelte-check": "^3.0.1", "ts-node": "10.9.1", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts index bfa29df7d549..779757c8807f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts @@ -8,6 +8,7 @@ if (!testEnv) { } const port = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -23,8 +24,9 @@ const config: PlaywrightTestConfig = { */ timeout: 10000, }, + workers: 1, /* Run tests in files in parallel */ - fullyParallel: true, + fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* `next dev` is incredibly buggy with the app dir */ @@ -61,8 +63,8 @@ const config: PlaywrightTestConfig = { { command: testEnv === 'development' - ? `pnpm wait-port ${port} && pnpm dev --port ${port}` - : `pnpm wait-port ${port} && pnpm preview --port ${port}`, + ? `pnpm wait-port ${eventProxyPort} && pnpm dev --port ${port}` + : `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${port}`, port, }, ], diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html index 117bd026151a..435cf39f2268 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts index 2d9cb9b02328..375b8d2c170a 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts @@ -9,9 +9,8 @@ Sentry.init({ tracesSampleRate: 1.0, }); -const myErrorHandler = ({ error, event }: any) => { - console.error('An error occurred on the server side:', error, event); -}; +// not logging anything to console to avoid noise in the test output +const myErrorHandler = ({ error, event }: any) => {}; export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte new file mode 100644 index 000000000000..8b7db6f720bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte index 5982b0ae37dd..aeb12d58603f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte @@ -1,2 +1,26 @@

Welcome to SvelteKit

Visit kit.svelte.dev to read the documentation

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts new file mode 100644 index 000000000000..d0e4371c594b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts @@ -0,0 +1,3 @@ +export const GET = () => { + return new Response(JSON.stringify({ users: ['alice', 'bob', 'carol'] })); +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte new file mode 100644 index 000000000000..ba6b464e9324 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Client error

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts new file mode 100644 index 000000000000..17dd53fb5bbb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts @@ -0,0 +1,6 @@ +export const load = async () => { + throw new Error('Server Load Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte new file mode 100644 index 000000000000..3a0942971d06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server load error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte new file mode 100644 index 000000000000..3d682e7e3462 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server Route error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts new file mode 100644 index 000000000000..298240827714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts @@ -0,0 +1,7 @@ +export const load = async ({ fetch }) => { + const res = await fetch('/server-route-error'); + const data = await res.json(); + return { + msg: data, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts new file mode 100644 index 000000000000..f1a4b94b7706 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts @@ -0,0 +1,6 @@ +export const GET = async () => { + throw new Error('Server Route Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte new file mode 100644 index 000000000000..dc2d311a0ece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte @@ -0,0 +1,17 @@ + + +

Universal load error

+ +

+ To trigger from client: Load on another route, then navigate to this route. +

+ +

+ To trigger from server: Load on this route +

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts new file mode 100644 index 000000000000..3d72bf4a890f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts @@ -0,0 +1,8 @@ +import { browser } from '$app/environment'; + +export const load = async () => { + throw new Error(`Universal Load Error (${browser ? 'browser' : 'server'})`); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte new file mode 100644 index 000000000000..563c51e8c850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte @@ -0,0 +1,14 @@ + + +

Fetching in universal load

+ +

Here's a list of a few users:

+ +
    + {#each data.users as user} +
  • {user}
  • + {/each} +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts new file mode 100644 index 000000000000..63c1ee68e1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts @@ -0,0 +1,5 @@ +export const load = async ({ fetch }) => { + const usersRes = await fetch('/api/users'); + const data = await usersRes.json(); + return { users: data.users }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts new file mode 100644 index 000000000000..a34c5450f682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async () => { + return { + msg: 'Hi everyone!', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte new file mode 100644 index 000000000000..aa804a4518fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte @@ -0,0 +1,10 @@ + +

+ All Users: +

+ +

+ message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts new file mode 100644 index 000000000000..9388f3927018 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async ({ params }) => { + return { + msg: `This is a special message for user ${params.id}`, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte new file mode 100644 index 000000000000..d348a8c57dad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte @@ -0,0 +1,14 @@ + + +

Route with dynamic params

+ +

+ User id: {$page.params.id} +

+ +

+ Secret message for user: {data.msg} +

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 new file mode 100644 index 000000000000..7cd1a545165b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../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 expect(page.getByText('Client error')).toBeVisible(); + + const errorEventPromise = waitForError('sveltekit', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; + }); + + const clickPromise = page.getByText('Throw error').click(); + + const [errorEvent, _] = await Promise.all([errorEventPromise, clickPromise]); + + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: expect.stringContaining('HTMLButtonElement'), + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); + + test('captures universal load error', async ({ page }) => { + await waitForInitialPageload(page); + await page.reload(); + + const errorEventPromise = waitForError('sveltekit', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (browser)'; + }); + + // navigating triggers the error on the client + await page.getByText('Universal Load error').click(); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); +}); 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 new file mode 100644 index 000000000000..5493659b19db --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test.describe('server-side errors', () => { + test('captures universal load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (server)'; + }); + + await page.goto('/universal-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load$1', + lineno: 3, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Load Error'; + }); + + await page.goto('/server-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load$1', + lineno: 3, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server route (GET) error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Route Error'; + }); + + await page.goto('/server-route-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + filename: 'app:///_server.ts.js', + function: 'GET', + lineno: 2, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ + runtime: 'node', + transaction: 'GET /server-route-error', + }); + }); +}); 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 new file mode 100644 index 000000000000..e0d3d16df1ab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts @@ -0,0 +1,124 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server.js'; +import { waitForInitialPageload } from '../utils.js'; + +test('sends a pageload transaction', async ({ page }) => { + const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toMatchObject({ + transaction: '/', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + }); +}); + +test('captures a distributed pageload trace', async ({ page }) => { + await page.goto('/users/123xyz'); + + const clientTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === '/users/[id]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + const [clientTxnEvent, serverTxnEvent] = await Promise.all([clientTxnEventPromise, serverTxnEventPromise]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users/[id]', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users/[id]', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + // weird but server txn is parent of client txn + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); +}); + +test('captures a distributed navigation trace', async ({ page }) => { + await waitForInitialPageload(page); + + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === '/users/[id]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with Params').click(); + + const [clientTxnEvent, serverTxnEvent, _1] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users/[id]', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users/[id]', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts deleted file mode 100644 index 7d621af34dcf..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect, test } from '@playwright/test'; -import axios, { AxiosError } from 'axios'; -// @ts-expect-error ok ok -import { waitForTransaction } from '../event-proxy-server.ts'; - -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 a pageload transaction', async ({ page }) => { - const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => { - return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; - }); - - await page.goto('/'); - - const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; - - 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/sveltekit/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts new file mode 100644 index 000000000000..2886873bb8fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts @@ -0,0 +1,53 @@ +import { Page } from '@playwright/test'; +import { waitForTransaction } from './event-proxy-server'; + +/** + * Helper function that waits for the initial pageload to complete. + * + * This function + * - loads the given route ("/" by default) + * - waits for SvelteKit's hydration + * - waits for the pageload transaction to be sent (doesn't assert on it though) + * + * Useful for tests that test outcomes of _navigations_ after an initial pageload. + * Waiting on the pageload transaction excludes edge cases where navigations occur + * so quickly that the pageload idle transaction is still active. This might lead + * to cases where the routing span would be attached to the pageload transaction + * and hence eliminates a lot of flakiness. + * + */ +export async function waitForInitialPageload( + page: Page, + opts?: { route?: string; parameterizedRoute?: string; debug: boolean }, +) { + const route = opts?.route ?? '/'; + const txnName = opts?.parameterizedRoute ?? route; + const debug = opts?.debug ?? false; + + const clientPageloadTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + debug && + console.log({ + txn: txnEvent?.transaction, + op: txnEvent.contexts?.trace?.op, + trace: txnEvent.contexts?.trace?.trace_id, + span: txnEvent.contexts?.trace?.span_id, + parent: txnEvent.contexts?.trace?.parent_span_id, + }); + + return txnEvent?.transaction === txnName && txnEvent.contexts?.trace?.op === 'pageload'; + }); + + await Promise.all([ + page.goto(route), + // the test app adds the "hydrated" class to the body when hydrating + page.waitForSelector('body.hydrated'), + // also waiting for the initial pageload txn so that later navigations don't interfere + clientPageloadTxnEventPromise, + ]); + + // let's add a buffer because it seems like the hydrated flag isn't enough :( + // guess: The layout finishes hydration/mounting before the components within finish + // await page.waitForTimeout(10_000); + + debug && console.log('hydrated'); +} diff --git a/package.json b/package.json index da7149544f46..9a226b3d8408 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "clean:build": "lerna run clean", "clean:caches": "yarn rimraf eslintcache .nxcache && yarn jest --clearCache", "clean:deps": "lerna clean --yes && rm -rf node_modules && yarn", - "clean:all": "run-s clean:build clean:caches clean:deps", + "clean:tarballs": "rimraf **/*.tgz", + "clean:all": "run-s clean:build clean:tarballs clean:caches clean:deps", "codecov": "codecov", "fix": "run-s fix:biome fix:prettier fix:lerna", "fix:lerna": "lerna run fix", From 8bec42e0285ee301e8fc9bcaf02046daf48e0495 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 31 Jan 2024 16:58:35 +0100 Subject: [PATCH 21/68] ref: Deprecate non-callback based `continueTrace` (#10301) --- MIGRATION.md | 26 ++ packages/astro/src/server/middleware.ts | 168 +++++++------ packages/astro/test/server/middleware.test.ts | 39 --- packages/core/src/tracing/trace.ts | 88 ++++--- packages/core/test/lib/tracing/trace.test.ts | 1 + .../src/common/utils/edgeWrapperUtils.ts | 82 +++---- .../src/common/wrapApiHandlerWithSentry.ts | 230 +++++++++--------- .../wrapGenerationFunctionWithSentry.ts | 1 + .../common/wrapServerComponentWithSentry.ts | 1 + packages/serverless/src/awslambda.ts | 31 ++- packages/serverless/src/gcpfunction/http.ts | 99 ++++---- packages/serverless/test/awslambda.test.ts | 47 ---- packages/serverless/test/gcpfunction.test.ts | 43 +--- 13 files changed, 392 insertions(+), 464 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index d52fd21f07fc..1f157fc735cc 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -197,6 +197,32 @@ be removed. Instead, use the new performance APIs: You can [read more about the new performance APIs here](./docs/v8-new-performance-apis.md). +## Deprecate variations of `Sentry.continueTrace()` + +The version of `Sentry.continueTrace()` which does not take a callback argument will be removed in favor of the version +that does. Additionally, the callback argument will not receive an argument with the next major version. + +Use `Sentry.continueTrace()` as follows: + +```ts +app.get('/your-route', req => { + Sentry.withIsolationScope(isolationScope => { + Sentry.continueTrace( + { + sentryTrace: req.headers.get('sentry-trace'), + baggage: req.headers.get('baggage'), + }, + () => { + // All events recorded in this callback will be associated with the incoming trace. For example: + Sentry.startSpan({ name: '/my-route' }, async () => { + await doExpensiveWork(); + }); + }, + ); + }); +}); +``` + ## Deprecate `Sentry.lastEventId()` and `hub.lastEventId()` `Sentry.lastEventId()` sometimes causes race conditions, so we are deprecating it in favour of the `beforeSend` diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 8a7cc3d90384..cd59d2f73344 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -94,97 +94,95 @@ async function instrumentRequest( const { method, headers } = ctx.request; - const traceCtx = continueTrace({ - sentryTrace: headers.get('sentry-trace') || undefined, - baggage: headers.get('baggage'), - }); + return continueTrace( + { + sentryTrace: headers.get('sentry-trace') || undefined, + baggage: headers.get('baggage'), + }, + async () => { + const allHeaders: Record = {}; - const allHeaders: Record = {}; + if (options.trackHeaders) { + headers.forEach((value, key) => { + allHeaders[key] = value; + }); + } - if (options.trackHeaders) { - headers.forEach((value, key) => { - allHeaders[key] = value; - }); - } + if (options.trackClientIp) { + getCurrentScope().setUser({ ip_address: ctx.clientAddress }); + } - if (options.trackClientIp) { - getCurrentScope().setUser({ ip_address: ctx.clientAddress }); - } + try { + const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); + const source = interpolatedRoute ? 'route' : 'url'; + // storing res in a variable instead of directly returning is necessary to + // invoke the catch block if next() throws + const res = await startSpan( + { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', + }, + name: `${method} ${interpolatedRoute || ctx.url.pathname}`, + op: 'http.server', + status: 'ok', + data: { + method, + url: stripUrlQueryAndFragment(ctx.url.href), + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + ...(ctx.url.search && { 'http.query': ctx.url.search }), + ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), + ...(options.trackHeaders && { headers: allHeaders }), + }, + }, + async span => { + const originalResponse = await next(); - try { - const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); - const source = interpolatedRoute ? 'route' : 'url'; - // storing res in a variable instead of directly returning is necessary to - // invoke the catch block if next() throws - const res = await startSpan( - { - ...traceCtx, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', - }, - name: `${method} ${interpolatedRoute || ctx.url.pathname}`, - op: 'http.server', - status: 'ok', - metadata: { - // eslint-disable-next-line deprecation/deprecation - ...traceCtx?.metadata, - }, - data: { - method, - url: stripUrlQueryAndFragment(ctx.url.href), - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, - ...(ctx.url.search && { 'http.query': ctx.url.search }), - ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), - ...(options.trackHeaders && { headers: allHeaders }), - }, - }, - async span => { - const originalResponse = await next(); - - if (span && originalResponse.status) { - setHttpStatus(span, originalResponse.status); - } - - const scope = getCurrentScope(); - const client = getClient(); - const contentType = originalResponse.headers.get('content-type'); - - const isPageloadRequest = contentType && contentType.startsWith('text/html'); - if (!isPageloadRequest || !client) { - return originalResponse; - } - - // Type case necessary b/c the body's ReadableStream type doesn't include - // the async iterator that is actually available in Node - // We later on use the async iterator to read the body chunks - // see https://github.com/microsoft/TypeScript/issues/39051 - const originalBody = originalResponse.body as NodeJS.ReadableStream | null; - if (!originalBody) { - return originalResponse; - } - - const decoder = new TextDecoder(); - - const newResponseStream = new ReadableStream({ - start: async controller => { - for await (const chunk of originalBody) { - const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); - const modifiedHtml = addMetaTagToHead(html, scope, client, span); - controller.enqueue(new TextEncoder().encode(modifiedHtml)); + if (span && originalResponse.status) { + setHttpStatus(span, originalResponse.status); } - controller.close(); - }, - }); - return new Response(newResponseStream, originalResponse); - }, - ); - return res; - } catch (e) { - sendErrorToSentry(e); - throw e; - } - // TODO: flush if serverless (first extract function) + const scope = getCurrentScope(); + const client = getClient(); + const contentType = originalResponse.headers.get('content-type'); + + const isPageloadRequest = contentType && contentType.startsWith('text/html'); + if (!isPageloadRequest || !client) { + return originalResponse; + } + + // Type case necessary b/c the body's ReadableStream type doesn't include + // the async iterator that is actually available in Node + // We later on use the async iterator to read the body chunks + // see https://github.com/microsoft/TypeScript/issues/39051 + const originalBody = originalResponse.body as NodeJS.ReadableStream | null; + if (!originalBody) { + return originalResponse; + } + + const decoder = new TextDecoder(); + + const newResponseStream = new ReadableStream({ + start: async controller => { + for await (const chunk of originalBody) { + const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); + const modifiedHtml = addMetaTagToHead(html, scope, client, span); + controller.enqueue(new TextEncoder().encode(modifiedHtml)); + } + controller.close(); + }, + }); + + return new Response(newResponseStream, originalResponse); + }, + ); + return res; + } catch (e) { + sendErrorToSentry(e); + throw e; + } + // TODO: flush if serverless (first extract function) + }, + ); } /** diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index c641f5ac6177..a83de3cb0eb2 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -66,7 +66,6 @@ describe('sentryMiddleware', () => { url: 'https://mydomain.io/users/123/details', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - metadata: {}, name: 'GET /users/[id]/details', op: 'http.server', status: 'ok', @@ -104,7 +103,6 @@ describe('sentryMiddleware', () => { url: 'http://localhost:1234/a%xx', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, - metadata: {}, name: 'GET a%xx', op: 'http.server', status: 'ok', @@ -144,43 +142,6 @@ describe('sentryMiddleware', () => { }); }); - it('attaches tracing headers', async () => { - const middleware = handleRequest(); - const ctx = { - request: { - method: 'GET', - url: '/users', - headers: new Headers({ - 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', - baggage: 'sentry-release=1.0.0', - }), - }, - params: {}, - url: new URL('https://myDomain.io/users/'), - }; - const next = vi.fn(() => nextResult); - - // @ts-expect-error, a partial ctx object is fine here - await middleware(ctx, next); - - expect(startSpanSpy).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }), - metadata: { - dynamicSamplingContext: { - release: '1.0.0', - }, - }, - parentSampled: true, - parentSpanId: '1234567890123456', - traceId: '12345678901234567890123456789012', - }), - expect.any(Function), // the `next` function - ); - }); - it('attaches client IP and request headers if options are set', async () => { const middleware = handleRequest({ trackClientIp: true, trackHeaders: true }); const ctx = { diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index d8109d6a9179..1f8b45d5fd97 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,6 +1,5 @@ import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; -import type { propagationContextFromHeaders } from '@sentry/utils'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -225,31 +224,58 @@ export function getActiveSpan(): Span | undefined { return getCurrentScope().getSpan(); } -export function continueTrace({ - sentryTrace, - baggage, -}: { - sentryTrace: Parameters[0]; - baggage: Parameters[1]; -}): Partial; -export function continueTrace( - { +interface ContinueTrace { + /** + * Continue a trace from `sentry-trace` and `baggage` values. + * These values can be obtained from incoming request headers, + * or in the browser from `` and `` HTML tags. + * + * @deprecated Use the version of this function taking a callback as second parameter instead: + * + * ``` + * Sentry.continueTrace(sentryTrace: '...', baggage: '...' }, () => { + * // ... + * }) + * ``` + * + */ + ({ sentryTrace, baggage, }: { - sentryTrace: Parameters[0]; - baggage: Parameters[1]; - }, - callback: (transactionContext: Partial) => V, -): V; -/** - * Continue a trace from `sentry-trace` and `baggage` values. - * These values can be obtained from incoming request headers, - * or in the browser from `` and `` HTML tags. - * - * The callback receives a transactionContext that may be used for `startTransaction` or `startSpan`. - */ -export function continueTrace( + // eslint-disable-next-line deprecation/deprecation + sentryTrace: Parameters[0]; + // eslint-disable-next-line deprecation/deprecation + baggage: Parameters[1]; + }): Partial; + + /** + * Continue a trace from `sentry-trace` and `baggage` values. + * These values can be obtained from incoming request headers, or in the browser from `` + * and `` HTML tags. + * + * Spans started with `startSpan`, `startSpanManual` and `startInactiveSpan`, within the callback will automatically + * be attached to the incoming trace. + * + * Deprecation notice: In the next major version of the SDK the provided callback will not receive a transaction + * context argument. + */ + ( + { + sentryTrace, + baggage, + }: { + // eslint-disable-next-line deprecation/deprecation + sentryTrace: Parameters[0]; + // eslint-disable-next-line deprecation/deprecation + baggage: Parameters[1]; + }, + // TODO(v8): Remove parameter from this callback. + callback: (transactionContext: Partial) => V, + ): V; +} + +export const continueTrace: ContinueTrace = ( { sentryTrace, baggage, @@ -260,12 +286,14 @@ export function continueTrace( baggage: Parameters[1]; }, callback?: (transactionContext: Partial) => V, -): V | Partial { +): V | Partial => { // TODO(v8): Change this function so it doesn't do anything besides setting the propagation context on the current scope: /* - const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); - getCurrentScope().setPropagationContext(propagationContext); - return; + return withScope((scope) => { + const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); + scope.setPropagationContext(propagationContext); + return callback(); + }) */ const currentScope = getCurrentScope(); @@ -293,8 +321,10 @@ export function continueTrace( return transactionContext; } - return callback(transactionContext); -} + return runWithAsyncContext(() => { + return callback(transactionContext); + }); +}; function createChildSpanOrTransaction( hub: Hub, diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 7cb09e1569ec..265a34195f71 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -758,6 +758,7 @@ describe('continueTrace', () => { traceId: '12312012123120121231201212312012', }; + // eslint-disable-next-line deprecation/deprecation const ctx = continueTrace({ sentryTrace: '12312012123120121231201212312012-1121201211212012-0', baggage: undefined, diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index fd98fe2328ee..8597228f6e83 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -32,52 +32,52 @@ export function withEdgeWrapping( baggage = req.headers.get('baggage'); } - const transactionContext = continueTrace({ - sentryTrace, - baggage, - }); - - return startSpan( + return continueTrace( { - ...transactionContext, - name: options.spanDescription, - op: options.spanOp, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', - }, - metadata: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.metadata, - request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined, - }, + sentryTrace, + baggage, }, - async span => { - const handlerResult = await handleCallbackErrors( - () => handler.apply(this, args), - error => { - captureException(error, { - mechanism: { - type: 'instrument', - handled: false, - data: { - function: options.mechanismFunctionName, - }, - }, - }); + () => { + return startSpan( + { + name: options.spanDescription, + op: options.spanOp, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', + }, + metadata: { + request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined, + }, }, - ); + async span => { + const handlerResult = await handleCallbackErrors( + () => handler.apply(this, args), + error => { + captureException(error, { + mechanism: { + type: 'instrument', + handled: false, + data: { + function: options.mechanismFunctionName, + }, + }, + }); + }, + ); - if (span) { - if (handlerResult instanceof Response) { - setHttpStatus(span, handlerResult.status); - } else { - span.setStatus('ok'); - } - } + if (span) { + if (handlerResult instanceof Response) { + setHttpStatus(span, handlerResult.status); + } else { + span.setStatus('ok'); + } + } - return handlerResult; + return handlerResult; + }, + ).finally(() => flushQueue()); }, - ).finally(() => flushQueue()); + ); }; } diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 61a08ba18891..62124e46912e 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -78,130 +78,130 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri addTracingExtensions(); - return runWithAsyncContext(async () => { - const transactionContext = continueTrace({ - sentryTrace: req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined, - baggage: req.headers?.baggage, - }); - - // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) - let reqPath = parameterizedRoute; - - // If not, fake it by just replacing parameter values with their names, hoping that none of them match either - // each other or any hard-coded parts of the path - if (!reqPath) { - const url = `${req.url}`; - // pull off query string, if any - reqPath = stripUrlQueryAndFragment(url); - // Replace with placeholder - if (req.query) { - for (const [key, value] of Object.entries(req.query)) { - reqPath = reqPath.replace(`${value}`, `[${key}]`); - } - } - } - - const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - - getCurrentScope().setSDKProcessingMetadata({ request: req }); - - return startSpanManual( + return runWithAsyncContext(() => { + return continueTrace( { - ...transactionContext, - name: `${reqMethod}${reqPath}`, - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', - }, - metadata: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.metadata, - request: req, - }, + sentryTrace: req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined, + baggage: req.headers?.baggage, }, - async span => { - // eslint-disable-next-line @typescript-eslint/unbound-method - res.end = new Proxy(res.end, { - apply(target, thisArg, argArray) { - if (span) { - setHttpStatus(span, res.statusCode); - span.end(); - } - if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { - target.apply(thisArg, argArray); - } else { - // flushQueue will not reject - // eslint-disable-next-line @typescript-eslint/no-floating-promises - flushQueue().then(() => { - target.apply(thisArg, argArray); - }); + () => { + // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) + let reqPath = parameterizedRoute; + + // If not, fake it by just replacing parameter values with their names, hoping that none of them match either + // each other or any hard-coded parts of the path + if (!reqPath) { + const url = `${req.url}`; + // pull off query string, if any + reqPath = stripUrlQueryAndFragment(url); + // Replace with placeholder + if (req.query) { + for (const [key, value] of Object.entries(req.query)) { + reqPath = reqPath.replace(`${value}`, `[${key}]`); } - }, - }); - - try { - const handlerResult = await wrappingTarget.apply(thisArg, args); - if ( - process.env.NODE_ENV === 'development' && - !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && - !res.finished - // TODO(v8): Remove this warning? - // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating. - // Warning suppression on Next.JS is only necessary in that case. - ) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[sentry] If Next.js logs a warning "API resolved without sending a response", it\'s a false positive, which may happen when you use `withSentry` manually to wrap your routes. To suppress this warning, set `SENTRY_IGNORE_API_RESOLUTION_ERROR` to 1 in your env. To suppress the nextjs warning, use the `externalResolver` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).', - ); - }); } + } - return handlerResult; - } catch (e) { - // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can - // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced - // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a - // way to prevent it from actually being reported twice.) - const objectifiedErr = objectify(e); - - captureException(objectifiedErr, { - mechanism: { - type: 'instrument', - handled: false, - data: { - wrapped_handler: wrappingTarget.name, - function: 'withSentry', - }, - }, - }); + const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet - // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that - // the transaction was error-free - res.statusCode = 500; - res.statusMessage = 'Internal Server Error'; + getCurrentScope().setSDKProcessingMetadata({ request: req }); - if (span) { - setHttpStatus(span, res.statusCode); - span.end(); - } + return startSpanManual( + { + name: `${reqMethod}${reqPath}`, + op: 'http.server', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', + }, + metadata: { + request: req, + }, + }, + async span => { + // eslint-disable-next-line @typescript-eslint/unbound-method + res.end = new Proxy(res.end, { + apply(target, thisArg, argArray) { + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); + } + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + target.apply(thisArg, argArray); + } else { + // flushQueue will not reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + flushQueue().then(() => { + target.apply(thisArg, argArray); + }); + } + }, + }); - // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors - // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the - // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not - // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already - // be finished and the queue will already be empty, so effectively it'll just no-op.) - if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { - await flushQueue(); - } + try { + const handlerResult = await wrappingTarget.apply(thisArg, args); + if ( + process.env.NODE_ENV === 'development' && + !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && + !res.finished + // TODO(v8): Remove this warning? + // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating. + // Warning suppression on Next.JS is only necessary in that case. + ) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[sentry] If Next.js logs a warning "API resolved without sending a response", it\'s a false positive, which may happen when you use `withSentry` manually to wrap your routes. To suppress this warning, set `SENTRY_IGNORE_API_RESOLUTION_ERROR` to 1 in your env. To suppress the nextjs warning, use the `externalResolver` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).', + ); + }); + } + + return handlerResult; + } catch (e) { + // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can + // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced + // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a + // way to prevent it from actually being reported twice.) + const objectifiedErr = objectify(e); + + captureException(objectifiedErr, { + mechanism: { + type: 'instrument', + handled: false, + data: { + wrapped_handler: wrappingTarget.name, + function: 'withSentry', + }, + }, + }); - // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it - // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark - // the error as already having been captured.) - throw objectifiedErr; - } + // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet + // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that + // the transaction was error-free + res.statusCode = 500; + res.statusMessage = 'Internal Server Error'; + + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); + } + + // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors + // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the + // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not + // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already + // be finished and the queue will already be empty, so effectively it'll just no-op.) + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + await flushQueue(); + } + + // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it + // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark + // the error as already having been captured.) + throw objectifiedErr; + } + }, + ); }, ); }); diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 5e6a051ffcfb..ec4fe9048900 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -47,6 +47,7 @@ export function wrapGenerationFunctionWithSentry a } return runWithAsyncContext(() => { + // eslint-disable-next-line deprecation/deprecation const transactionContext = continueTrace({ baggage: headers?.get('baggage'), sentryTrace: headers?.get('sentry-trace') ?? undefined, diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index f8b6c5698550..59a608406e09 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -37,6 +37,7 @@ export function wrapServerComponentWithSentry any> ? winterCGHeadersToDict(context.headers) : {}; + // eslint-disable-next-line deprecation/deprecation const transactionContext = continueTrace({ // eslint-disable-next-line deprecation/deprecation sentryTrace: context.sentryTraceHeader ?? completeHeadersDict['sentry-trace'], diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 10f0e0e29c81..240dff84eebc 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -354,24 +354,23 @@ export function wrapHandler( : undefined; const baggage = eventWithHeaders.headers?.baggage; - const continueTraceContext = continueTrace({ sentryTrace, baggage }); - - return startSpanManual( - { - name: context.functionName, - op: 'function.aws.lambda', - ...continueTraceContext, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', + return continueTrace({ sentryTrace, baggage }, () => { + return startSpanManual( + { + name: context.functionName, + op: 'function.aws.lambda', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', + }, }, - }, - span => { - enhanceScopeWithTransactionData(getCurrentScope(), context); + span => { + enhanceScopeWithTransactionData(getCurrentScope(), context); - return processResult(span); - }, - ); + return processResult(span); + }, + ); + }); } return withScope(async () => { diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index a90dd3a0423c..e02093acf438 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -77,58 +77,57 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { + return startSpanManual( + { + name: `${reqMethod} ${reqUrl}`, + op: 'function.gcp.http', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + }, }, - }, - span => { - getCurrentScope().setSDKProcessingMetadata({ - request: req, - requestDataOptionsFromGCPWrapper: options.addRequestDataToEventOptions, - }); - - 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 - res.end = function (chunk?: any | (() => void), encoding?: string | (() => void), cb?: () => void): any { - if (span) { - setHttpStatus(span, res.statusCode); - span.end(); + span => { + getCurrentScope().setSDKProcessingMetadata({ + request: req, + requestDataOptionsFromGCPWrapper: options.addRequestDataToEventOptions, + }); + + 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/no-floating-promises - flush(options.flushTimeout) - .then(null, e => { - DEBUG_BUILD && logger.error(e); - }) - .then(() => { - _end.call(this, chunk, encoding, cb); - }); - }; - - return handleCallbackErrors( - () => fn(req, res), - err => { - captureException(err, scope => markEventUnhandled(scope)); - }, - ); - }, - ); + // eslint-disable-next-line @typescript-eslint/unbound-method + const _end = res.end; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + res.end = function (chunk?: any | (() => void), encoding?: string | (() => void), cb?: () => void): any { + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + flush(options.flushTimeout) + .then(null, e => { + DEBUG_BUILD && logger.error(e); + }) + .then(() => { + _end.call(this, chunk, encoding, cb); + }); + }; + + return handleCallbackErrors( + () => fn(req, res), + err => { + captureException(err, scope => markEventUnhandled(scope)); + }, + ); + }, + ); + }); }; } diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 57feede5a102..772502057a34 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -247,7 +247,6 @@ describe('AWSLambda', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: {}, }; expect(rv).toStrictEqual(42); @@ -277,7 +276,6 @@ describe('AWSLambda', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: {}, }; expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -301,42 +299,6 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); }); - test('incoming trace headers are correctly parsed and used', async () => { - expect.assertions(1); - - fakeEvent.headers = { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', - baggage: 'sentry-release=2.12.1,maisey=silly,charlie=goofy', - }; - - const handler: Handler = (_event, _context, callback) => { - expect(mockStartSpanManual).toBeCalledWith( - expect.objectContaining({ - parentSpanId: '1121201211212012', - parentSampled: false, - op: 'function.aws.lambda', - name: 'functionName', - traceId: '12312012123120121231201212312012', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', - }, - metadata: { - dynamicSamplingContext: { - release: '2.12.1', - }, - }, - }), - expect.any(Function), - ); - - callback(undefined, { its: 'fine' }); - }; - - const wrappedHandler = wrapHandler(handler); - await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - }); - test('capture error', async () => { expect.assertions(10); @@ -347,20 +309,15 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler); try { - fakeEvent.headers = { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0' }; await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - parentSampled: false, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: { dynamicSamplingContext: {} }, }; expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -390,7 +347,6 @@ describe('AWSLambda', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: {}, }; expect(rv).toStrictEqual(42); @@ -431,7 +387,6 @@ describe('AWSLambda', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: {}, }; expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -474,7 +429,6 @@ describe('AWSLambda', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: {}, }; expect(rv).toStrictEqual(42); @@ -515,7 +469,6 @@ describe('AWSLambda', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, - metadata: {}, }; expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 1fc58c37fdce..cde69e6b22d2 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -164,7 +164,6 @@ describe('GCPFunction', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', }, - metadata: {}, }; expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -172,38 +171,6 @@ describe('GCPFunction', () => { expect(mockFlush).toBeCalledWith(2000); }); - test('incoming trace headers are correctly parsed and used', async () => { - const handler: HttpFunction = (_req, res) => { - res.statusCode = 200; - res.end(); - }; - const wrappedHandler = wrapHttpFunction(handler); - const traceHeaders = { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', - baggage: 'sentry-release=2.12.1,maisey=silly,charlie=goofy', - }; - await handleHttp(wrappedHandler, traceHeaders); - - const fakeTransactionContext = { - name: 'POST /path', - op: 'function.gcp.http', - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - parentSampled: false, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', - }, - metadata: { - dynamicSamplingContext: { - release: '2.12.1', - }, - }, - }; - - expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - }); - test('capture error', async () => { const error = new Error('wat'); const handler: HttpFunction = (_req, _res) => { @@ -211,23 +178,15 @@ describe('GCPFunction', () => { }; const wrappedHandler = wrapHttpFunction(handler); - const trace_headers: { [key: string]: string } = { - 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', - }; - - await handleHttp(wrappedHandler, trace_headers); + await handleHttp(wrappedHandler); const fakeTransactionContext = { name: 'POST /path', op: 'function.gcp.http', - traceId: '12312012123120121231201212312012', - parentSpanId: '1121201211212012', - parentSampled: false, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', }, - metadata: { dynamicSamplingContext: {} }, }; expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); From 1f1ccf13b69c683cea00cb2681320201727966e6 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 31 Jan 2024 15:23:57 -0330 Subject: [PATCH 22/68] feat(replay): Bump `rrweb` to 2.10.0 (#10445) Fixes an issue where errors from `CanvasManager` were being captured into our clients's Sentry. Closes #10271 --- .../browser-integration-tests/package.json | 2 +- packages/replay-canvas/package.json | 2 +- packages/replay/package.json | 4 +- yarn.lock | 42 +++++++++---------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index d625d4fc3ca0..847db586afb0 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -46,7 +46,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.40.1", - "@sentry-internal/rrweb": "2.9.0", + "@sentry-internal/rrweb": "2.10.0", "@sentry/browser": "7.99.0", "@sentry/tracing": "7.99.0", "axios": "1.6.0", diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index b7f4c6a7675a..57abe8b43e64 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -56,7 +56,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/rrweb": "2.9.0" + "@sentry-internal/rrweb": "2.10.0" }, "dependencies": { "@sentry/core": "7.99.0", diff --git a/packages/replay/package.json b/packages/replay/package.json index 9615a3baf8b6..7c7eb6bd09d5 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -54,8 +54,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.99.0", - "@sentry-internal/rrweb": "2.9.0", - "@sentry-internal/rrweb-snapshot": "2.9.0", + "@sentry-internal/rrweb": "2.10.0", + "@sentry-internal/rrweb-snapshot": "2.10.0", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, diff --git a/yarn.lock b/yarn.lock index bcbeead21f52..d2946ed22906 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5407,33 +5407,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.9.0.tgz#dbb30c00a859156e9bfdfe701af85477fa082cbf" - integrity sha512-8jULvAmXunPfNChUCOhKSr4rRg7govoH7L/8XuRsK4++wJryjOJDO/zMnway5c3u03PKbFcZFcqCyKjaQQKcHg== +"@sentry-internal/rrdom@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.10.0.tgz#7f86667939a100bee2f82b6d459e275855ccc583" + integrity sha512-28G4W8BCdqI8GsO1SYkCBIwuizLwHrg8gE4u77v0zKpiaeIyZjYJ0QqhA/gMrTHLqrfI+FAwGXchnamjci45BA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.9.0" + "@sentry-internal/rrweb-snapshot" "2.10.0" -"@sentry-internal/rrweb-snapshot@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.9.0.tgz#f7b682992e70174547c495a4a6deae39136cecf2" - integrity sha512-oK8L3g41PFli1MpItYIFYCisCB+XjpqbEup0lVyTa/6wvKe0SOxZK9aUb/y03/2onSMmQ+FRkKLL6Kd0gHYJOA== +"@sentry-internal/rrweb-snapshot@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.10.0.tgz#fa894fad3110fa8b912e41eb328bd956581c0ac0" + integrity sha512-/bqbmCzEn8o/hki9Jrng6xIkjczYlPHTEv+C/NDT7Q8A7WJ9KqIpCkljqyoNrD2o9OtwFuPAVgKyIPRkZF9ZfA== -"@sentry-internal/rrweb-types@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.9.0.tgz#a70450ab7ca9884fd8d70bdb45dc214ed554956e" - integrity sha512-s3YhCvXzMM7byAfjHyCWmSOUBDbzUpWHWZj7FR6G8xa3nIrIePceziMc9wxEdqi7nCcmDHPc+kZ2GzDaZIrebA== +"@sentry-internal/rrweb-types@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.10.0.tgz#d9da0362c31c4e96b8649bbc9ab8bb380051caf3" + integrity sha512-nnwRrH0O8J+OsOEK3LeVruTv6JovZWEFywdacyfNt2LK7XTCG8182lU6bzPK3Ganb9ps2eOkJqOTRMYUZ1TrMA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.9.0" + "@sentry-internal/rrweb-snapshot" "2.10.0" -"@sentry-internal/rrweb@2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.9.0.tgz#a41af914baaf69c7a1e76d22d1780d50c3dfed0e" - integrity sha512-fDPYXWHOwt/PZzOklS17xPsjMsZ6D0K7CX3tvaDE4IkHCHM1PmGJhrXo05NL86WHhRKNKeRT3WQaokrrZzU5zA== +"@sentry-internal/rrweb@2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.10.0.tgz#a101f08f4b5de70145dbbdf70e7d2a0ac4d0d83e" + integrity sha512-S2xC0xxliCCgfowFImqIK6i9dfaEuTsLrzYkPxxX54OjqjrTsJw41aGxGfYPh+PP6nWMiURuOM5jRZrbvxoH4A== dependencies: - "@sentry-internal/rrdom" "2.9.0" - "@sentry-internal/rrweb-snapshot" "2.9.0" - "@sentry-internal/rrweb-types" "2.9.0" + "@sentry-internal/rrdom" "2.10.0" + "@sentry-internal/rrweb-snapshot" "2.10.0" + "@sentry-internal/rrweb-types" "2.10.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From 2000d7e14611769078d6c3e67a30905e090ec24e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 1 Feb 2024 09:20:47 +0100 Subject: [PATCH 23/68] feat(node-experimental): Update tracing integrations to functional style (#10443) This refactors the node-experimental performance integrations to functional style. Importantly, it also slightly updates the options for httpIntegration & nativeNodeFetchIntegration, with what they can/should be in v8: * There is no way to disable span creation for them (this is pretty hacky to do, and prob. not needed - not allowing this simplifies this a lot) * You can define filters for incoming/outgoing requests to filter them fully - that will filter them both for breadcrumbs and spans. * spans are not created anyhow when tracing is disabled. --- .github/workflows/build.yml | 1 + packages/node-experimental/src/index.ts | 13 ++ .../src/integrations/express.ts | 25 ++- .../src/integrations/fastify.ts | 26 ++- .../getAutoPerformanceIntegrations.ts | 91 +++------- .../src/integrations/graphql.ts | 27 ++- .../src/integrations/hapi.ts | 20 ++- .../src/integrations/http.ts | 164 ++++++++++++++---- .../src/integrations/index.ts | 1 + .../src/integrations/mongo.ts | 26 ++- .../src/integrations/mongoose.ts | 26 ++- .../src/integrations/mysql.ts | 20 ++- .../src/integrations/mysql2.ts | 26 ++- .../src/integrations/nest.ts | 20 ++- .../src/integrations/node-fetch.ts | 119 ++++++++++--- .../src/integrations/postgres.ts | 27 ++- .../src/integrations/prisma.ts | 23 ++- packages/node-experimental/src/sdk/init.ts | 12 +- .../src/sdk/spanProcessor.ts | 3 + .../test/integration/transactions.test.ts | 2 + 20 files changed, 531 insertions(+), 141 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 036cbccd5947..a46006fe5325 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,7 @@ jobs: node: - *shared - 'packages/node/**' + - 'packages/node-experimental/**' - 'dev-packages/node-integration-tests/**' deno: - *shared diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 3b208ea1d2d8..9338fae2183d 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -1,7 +1,20 @@ import { Integrations as CoreIntegrations } from '@sentry/core'; import * as NodeExperimentalIntegrations from './integrations'; +export { expressIntegration } from './integrations/express'; +export { fastifyIntegration } from './integrations/fastify'; +export { graphqlIntegration } from './integrations/graphql'; +export { httpIntegration } from './integrations/http'; +export { mongoIntegration } from './integrations/mongo'; +export { mongooseIntegration } from './integrations/mongoose'; +export { mysqlIntegration } from './integrations/mysql'; +export { mysql2Integration } from './integrations/mysql2'; +export { nestIntegration } from './integrations/nest'; +export { nativeNodeFetchIntegration } from './integrations/node-fetch'; +export { postgresIntegration } from './integrations/postgres'; +export { prismaIntegration } from './integrations/prisma'; +/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ export const Integrations = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, diff --git a/packages/node-experimental/src/integrations/express.ts b/packages/node-experimental/src/integrations/express.ts index 0bbe3a19a11d..1931038da714 100644 --- a/packages/node-experimental/src/integrations/express.ts +++ b/packages/node-experimental/src/integrations/express.ts @@ -1,14 +1,36 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _expressIntegration = (() => { + return { + name: 'Express', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new ExpressInstrumentation({ + requestHook(span) { + addOriginToSpan(span, 'auto.http.otel.express'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const expressIntegration = defineIntegration(_expressIntegration); + /** * Express integration * * Capture tracing data for express. + * @deprecated Use `expressIntegration()` instead. */ export class Express extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +45,7 @@ export class Express extends NodePerformanceIntegration implements Integra public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Express.id; } diff --git a/packages/node-experimental/src/integrations/fastify.ts b/packages/node-experimental/src/integrations/fastify.ts index 4d32037887b1..b34d267934aa 100644 --- a/packages/node-experimental/src/integrations/fastify.ts +++ b/packages/node-experimental/src/integrations/fastify.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _fastifyIntegration = (() => { + return { + name: 'Fastify', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new FastifyInstrumentation({ + requestHook(span) { + addOriginToSpan(span, 'auto.http.otel.fastify'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const fastifyIntegration = defineIntegration(_fastifyIntegration); + /** * Express integration * * Capture tracing data for fastify. + * + * @deprecated Use `fastifyIntegration()` instead. */ export class Fastify extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Fastify extends NodePerformanceIntegration implements Integra public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Fastify.id; } diff --git a/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts b/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts index 1a4200ab6fb0..77d772ce005b 100644 --- a/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts +++ b/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts @@ -1,73 +1,32 @@ import type { Integration } from '@sentry/types'; -import type { NodePerformanceIntegration } from './NodePerformanceIntegration'; -import { Express } from './express'; -import { Fastify } from './fastify'; -import { GraphQL } from './graphql'; -import { Hapi } from './hapi'; -import { Mongo } from './mongo'; -import { Mongoose } from './mongoose'; -import { Mysql } from './mysql'; -import { Mysql2 } from './mysql2'; -import { Nest } from './nest'; -import { Postgres } from './postgres'; -import { Prisma } from './prisma'; - -const INTEGRATIONS: (() => NodePerformanceIntegration)[] = [ - () => { - return new Express(); - }, - () => { - return new Fastify(); - }, - () => { - return new GraphQL(); - }, - () => { - return new Mongo(); - }, - () => { - return new Mongoose(); - }, - () => { - return new Mysql(); - }, - () => { - return new Mysql2(); - }, - () => { - return new Postgres(); - }, - () => { - return new Prisma(); - }, - () => { - return new Nest(); - }, - () => { - return new Hapi(); - }, -]; +import { expressIntegration } from './express'; +import { fastifyIntegration } from './fastify'; +import { graphqlIntegration } from './graphql'; +import { hapiIntegration } from './hapi'; +import { mongoIntegration } from './mongo'; +import { mongooseIntegration } from './mongoose'; +import { mysqlIntegration } from './mysql'; +import { mysql2Integration } from './mysql2'; +import { nestIntegration } from './nest'; +import { postgresIntegration } from './postgres'; +import { prismaIntegration } from './prisma'; /** - * Get auto-dsicovered performance integrations. - * Note that due to the way OpenTelemetry instrumentation works, this will generally still return Integrations - * for stuff that may not be installed. This is because Otel only instruments when the module is imported/required, - * so if the package is not required at all it will not be patched, and thus not instrumented. - * But the _Sentry_ Integration will still be added. - * This _may_ be a bit confusing because it shows all integrations as being installed in the debug logs, but this is - * technically not wrong because we install it (it just doesn't do anything). + * With OTEL, all performance integrations will be added, as OTEL only initializes them when the patched package is actually required. */ export function getAutoPerformanceIntegrations(): Integration[] { - const loadedIntegrations = INTEGRATIONS.map(tryLoad => { - try { - const integration = tryLoad(); - const isLoaded = integration.loadInstrumentations(); - return isLoaded ? integration : false; - } catch (_) { - return false; - } - }).filter(integration => !!integration) as Integration[]; - - return loadedIntegrations; + return [ + expressIntegration(), + fastifyIntegration(), + graphqlIntegration(), + mongoIntegration(), + mongooseIntegration(), + mysqlIntegration(), + mysql2Integration(), + postgresIntegration(), + prismaIntegration(), + nestIntegration(), + hapiIntegration(), + ]; } diff --git a/packages/node-experimental/src/integrations/graphql.ts b/packages/node-experimental/src/integrations/graphql.ts index b4a529df713e..576d049c44b9 100644 --- a/packages/node-experimental/src/integrations/graphql.ts +++ b/packages/node-experimental/src/integrations/graphql.ts @@ -1,14 +1,38 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _graphqlIntegration = (() => { + return { + name: 'Graphql', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new GraphQLInstrumentation({ + ignoreTrivialResolveSpans: true, + responseHook(span) { + addOriginToSpan(span, 'auto.graphql.otel.graphql'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const graphqlIntegration = defineIntegration(_graphqlIntegration); + /** * GraphQL integration * * Capture tracing data for GraphQL. + * + * @deprecated Use `graphqlIntegration()` instead. */ export class GraphQL extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +47,7 @@ export class GraphQL extends NodePerformanceIntegration implements Integra public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = GraphQL.id; } diff --git a/packages/node-experimental/src/integrations/hapi.ts b/packages/node-experimental/src/integrations/hapi.ts index 3f486e07961c..1376bcb49ccf 100644 --- a/packages/node-experimental/src/integrations/hapi.ts +++ b/packages/node-experimental/src/integrations/hapi.ts @@ -1,13 +1,30 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _hapiIntegration = (() => { + return { + name: 'Hapi', + setupOnce() { + registerInstrumentations({ + instrumentations: [new HapiInstrumentation()], + }); + }, + }; +}) satisfies IntegrationFn; + +export const hapiIntegration = defineIntegration(_hapiIntegration); + /** * Hapi integration * * Capture tracing data for Hapi. + * + * @deprecated Use `hapiIntegration()` instead. */ export class Hapi extends NodePerformanceIntegration implements Integration { /** @@ -22,6 +39,7 @@ export class Hapi extends NodePerformanceIntegration implements Integratio public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Hapi.id; } diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 66606bbf8258..6894ddc4ab2e 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -3,9 +3,9 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { addBreadcrumb, hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; +import { addBreadcrumb, defineIntegration, hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; import { _INTERNAL, getClient, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; -import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import type { EventProcessor, Hub, Integration, IntegrationFn } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; import { getIsolationScope, setIsolationScope } from '../sdk/api'; @@ -14,6 +14,97 @@ 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, req); + + // Update the isolation scope, isolate this request + if (getSpanKind(span) === SpanKind.SERVER) { + setIsolationScope(getIsolationScope().clone()); + } + }, + responseHook: (span, res) => { + if (_breadcrumbs) { + _addRequestBreadcrumb(span, res); + } + }, + }), + ]; + + registerInstrumentations({ + instrumentations, + }); + }, + }; +}) satisfies IntegrationFn; + +export const httpIntegration = defineIntegration(_httpIntegration); + +interface OldHttpOptions { /** * Whether breadcrumbs should be recorded for requests * Defaults to true @@ -39,6 +130,8 @@ interface HttpOptions { * * Create spans for outgoing requests * * Note that this integration is also needed for the Express integration to work! + * + * @deprecated Use `httpIntegration()` instead. */ export class Http implements Integration { /** @@ -65,7 +158,8 @@ export class Http implements Integration { /** * @inheritDoc */ - public constructor(options: HttpOptions = {}) { + public constructor(options: OldHttpOptions = {}) { + // eslint-disable-next-line deprecation/deprecation this.name = Http.id; this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; this._spans = typeof options.spans === 'undefined' ? undefined : options.spans; @@ -127,7 +221,7 @@ export class Http implements Integration { requireParentforOutgoingSpans: true, requireParentforIncomingSpans: false, requestHook: (span, req) => { - this._updateSpan(span, req); + _updateSpan(span, req); // Update the isolation scope, isolate this request if (getSpanKind(span) === SpanKind.SERVER) { @@ -135,7 +229,9 @@ export class Http implements Integration { } }, responseHook: (span, res) => { - this._addRequestBreadcrumb(span, res); + if (this._breadcrumbs) { + _addRequestBreadcrumb(span, res); + } }, }), ], @@ -148,39 +244,39 @@ export class Http implements Integration { public unregister(): void { this._unload?.(); } +} - /** Update the span with data we need. */ - private _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { - addOriginToSpan(span, 'auto.http.otel.http'); +/** Update the span with data we need. */ +function _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { + addOriginToSpan(span, 'auto.http.otel.http'); - if (getSpanKind(span) === SpanKind.SERVER) { - setSpanMetadata(span, { request }); - } + if (getSpanKind(span) === SpanKind.SERVER) { + setSpanMetadata(span, { request }); } +} - /** Add a breadcrumb for outgoing requests. */ - private _addRequestBreadcrumb(span: Span, response: IncomingMessage | ServerResponse): void { - if (!this._breadcrumbs || getSpanKind(span) !== SpanKind.CLIENT) { - return; - } +/** Add a breadcrumb for outgoing requests. */ +function _addRequestBreadcrumb(span: Span, response: IncomingMessage | ServerResponse): void { + if (getSpanKind(span) !== SpanKind.CLIENT) { + return; + } - const data = _INTERNAL.getRequestSpanData(span); - addBreadcrumb( - { - category: 'http', - data: { - status_code: response.statusCode, - ...data, - }, - type: 'http', + const data = _INTERNAL.getRequestSpanData(span); + addBreadcrumb( + { + category: 'http', + data: { + status_code: response.statusCode, + ...data, }, - { - 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, - }, - ); - } + 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/index.ts b/packages/node-experimental/src/integrations/index.ts index 7279f45c2dfc..a37c0f3b615e 100644 --- a/packages/node-experimental/src/integrations/index.ts +++ b/packages/node-experimental/src/integrations/index.ts @@ -23,6 +23,7 @@ export { LocalVariables, }; +/* eslint-disable deprecation/deprecation */ export { Express } from './express'; export { Http } from './http'; export { NodeFetch } from './node-fetch'; diff --git a/packages/node-experimental/src/integrations/mongo.ts b/packages/node-experimental/src/integrations/mongo.ts index f8be482be946..bcfaaaf1bc62 100644 --- a/packages/node-experimental/src/integrations/mongo.ts +++ b/packages/node-experimental/src/integrations/mongo.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mongoIntegration = (() => { + return { + name: 'Mongo', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new MongoDBInstrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mongo'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mongoIntegration = defineIntegration(_mongoIntegration); + /** * MongoDB integration * * Capture tracing data for MongoDB. + * + * @deprecated Use `mongoIntegration()` instead. */ export class Mongo extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Mongo extends NodePerformanceIntegration implements Integrati public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mongo.id; } diff --git a/packages/node-experimental/src/integrations/mongoose.ts b/packages/node-experimental/src/integrations/mongoose.ts index a5361a620bc2..a14c7d54a266 100644 --- a/packages/node-experimental/src/integrations/mongoose.ts +++ b/packages/node-experimental/src/integrations/mongoose.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mongooseIntegration = (() => { + return { + name: 'Mongoose', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new MongooseInstrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mongoose'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mongooseIntegration = defineIntegration(_mongooseIntegration); + /** * Mongoose integration * * Capture tracing data for Mongoose. + * + * @deprecated Use `mongooseIntegration()` instead. */ export class Mongoose extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Mongoose extends NodePerformanceIntegration implements Integr public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mongoose.id; } diff --git a/packages/node-experimental/src/integrations/mysql.ts b/packages/node-experimental/src/integrations/mysql.ts index 3973f07f4685..3cf0f4e42c87 100644 --- a/packages/node-experimental/src/integrations/mysql.ts +++ b/packages/node-experimental/src/integrations/mysql.ts @@ -1,13 +1,30 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mysqlIntegration = (() => { + return { + name: 'Mysql', + setupOnce() { + registerInstrumentations({ + instrumentations: [new MySQLInstrumentation({})], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mysqlIntegration = defineIntegration(_mysqlIntegration); + /** * MySQL integration * * Capture tracing data for mysql. + * + * @deprecated Use `mysqlIntegration()` instead. */ export class Mysql extends NodePerformanceIntegration implements Integration { /** @@ -22,6 +39,7 @@ export class Mysql extends NodePerformanceIntegration implements Integrati public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mysql.id; } diff --git a/packages/node-experimental/src/integrations/mysql2.ts b/packages/node-experimental/src/integrations/mysql2.ts index 9a87de98fd66..bb89d0aa01cb 100644 --- a/packages/node-experimental/src/integrations/mysql2.ts +++ b/packages/node-experimental/src/integrations/mysql2.ts @@ -1,14 +1,37 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _mysql2Integration = (() => { + return { + name: 'Mysql2', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new MySQL2Instrumentation({ + responseHook(span) { + addOriginToSpan(span, 'auto.db.otel.mysql2'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const mysql2Integration = defineIntegration(_mysql2Integration); + /** * MySQL2 integration * * Capture tracing data for mysql2 + * + * @deprecated Use `mysql2Integration()` instead. */ export class Mysql2 extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +46,7 @@ export class Mysql2 extends NodePerformanceIntegration implements Integrat public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Mysql2.id; } diff --git a/packages/node-experimental/src/integrations/nest.ts b/packages/node-experimental/src/integrations/nest.ts index b7e47b2f49c8..c03955f71193 100644 --- a/packages/node-experimental/src/integrations/nest.ts +++ b/packages/node-experimental/src/integrations/nest.ts @@ -1,13 +1,30 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _nestIntegration = (() => { + return { + name: 'Nest', + setupOnce() { + registerInstrumentations({ + instrumentations: [new NestInstrumentation({})], + }); + }, + }; +}) satisfies IntegrationFn; + +export const nestIntegration = defineIntegration(_nestIntegration); + /** * Nest framework integration * * Capture tracing data for nest. + * + * @deprecated Use `nestIntegration()` instead. */ export class Nest extends NodePerformanceIntegration implements Integration { /** @@ -22,6 +39,7 @@ export class Nest extends NodePerformanceIntegration implements Integratio public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Nest.id; } diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts index a2b7b61bdfc0..35bf982d286e 100644 --- a/packages/node-experimental/src/integrations/node-fetch.ts +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -1,9 +1,10 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; -import { addBreadcrumb, hasTracingEnabled } from '@sentry/core'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { addBreadcrumb, defineIntegration, hasTracingEnabled } from '@sentry/core'; import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; -import type { Integration } from '@sentry/types'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { parseSemver } from '@sentry/utils'; import type { NodeExperimentalClient } from '../types'; @@ -13,6 +14,70 @@ import { NodePerformanceIntegration } from './NodePerformanceIntegration'; const NODE_VERSION: ReturnType = parseSemver(process.versions.node); interface NodeFetchOptions { + /** + * Whether breadcrumbs should be recorded for requests. + * Defaults to true + */ + breadcrumbs?: boolean; + + /** + * Do not capture spans or breadcrumbs for outgoing fetch 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; +} + +const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => { + const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; + + function getInstrumentation(): [Instrumentation] | void { + // Only add NodeFetch if Node >= 16, as previous versions do not support it + if (!NODE_VERSION.major || NODE_VERSION.major < 16) { + return; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { FetchInstrumentation } = require('opentelemetry-instrumentation-fetch-node'); + return [ + new FetchInstrumentation({ + ignoreRequestHook: (request: { origin?: string }) => { + const url = request.origin; + return _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url); + }, + + onRequest: ({ span }: { span: Span }) => { + _updateSpan(span); + + if (_breadcrumbs) { + _addRequestBreadcrumb(span); + } + }, + }), + ]; + } catch (error) { + // Could not load instrumentation + } + } + + return { + name: 'NodeFetch', + setupOnce() { + const instrumentations = getInstrumentation(); + + if (instrumentations) { + registerInstrumentations({ + instrumentations, + }); + } + }, + }; +}) satisfies IntegrationFn; + +export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration); + +interface OldNodeFetchOptions { /** * Whether breadcrumbs should be recorded for requests * Defaults to true @@ -31,8 +96,10 @@ interface NodeFetchOptions { * This instrumentation does two things: * * Create breadcrumbs for outgoing requests * * Create spans for outgoing requests + * + * @deprecated Use `nativeNodeFetchIntegration()` instead. */ -export class NodeFetch extends NodePerformanceIntegration implements Integration { +export class NodeFetch extends NodePerformanceIntegration implements Integration { /** * @inheritDoc */ @@ -55,9 +122,10 @@ export class NodeFetch extends NodePerformanceIntegration impl /** * @inheritDoc */ - public constructor(options: NodeFetchOptions = {}) { + public constructor(options: OldNodeFetchOptions = {}) { super(options); + // eslint-disable-next-line deprecation/deprecation this.name = NodeFetch.id; this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; this._spans = typeof options.spans === 'undefined' ? undefined : options.spans; @@ -79,8 +147,11 @@ export class NodeFetch extends NodePerformanceIntegration impl return [ new FetchInstrumentation({ onRequest: ({ span }: { span: Span }) => { - this._updateSpan(span); - this._addRequestBreadcrumb(span); + _updateSpan(span); + + if (this._breadcrumbs) { + _addRequestBreadcrumb(span); + } }, }), ]; @@ -109,25 +180,25 @@ export class NodeFetch extends NodePerformanceIntegration impl public unregister(): void { this._unload?.(); } +} - /** Update the span with data we need. */ - private _updateSpan(span: Span): void { - addOriginToSpan(span, 'auto.http.otel.node_fetch'); - } - - /** Add a breadcrumb for outgoing requests. */ - private _addRequestBreadcrumb(span: Span): void { - if (!this._breadcrumbs || getSpanKind(span) !== SpanKind.CLIENT) { - return; - } +/** Update the span with data we need. */ +function _updateSpan(span: Span): void { + addOriginToSpan(span, 'auto.http.otel.node_fetch'); +} - const data = _INTERNAL.getRequestSpanData(span); - addBreadcrumb({ - category: 'http', - data: { - ...data, - }, - type: 'http', - }); +/** Add a breadcrumb for outgoing requests. */ +function _addRequestBreadcrumb(span: Span): void { + if (getSpanKind(span) !== SpanKind.CLIENT) { + return; } + + const data = _INTERNAL.getRequestSpanData(span); + addBreadcrumb({ + category: 'http', + data: { + ...data, + }, + type: 'http', + }); } diff --git a/packages/node-experimental/src/integrations/postgres.ts b/packages/node-experimental/src/integrations/postgres.ts index 85584f8a6507..91a6a710ffdd 100644 --- a/packages/node-experimental/src/integrations/postgres.ts +++ b/packages/node-experimental/src/integrations/postgres.ts @@ -1,14 +1,38 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _postgresIntegration = (() => { + return { + name: 'Postgres', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + new PgInstrumentation({ + requireParentSpan: true, + requestHook(span) { + addOriginToSpan(span, 'auto.db.otel.postgres'); + }, + }), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const postgresIntegration = defineIntegration(_postgresIntegration); + /** * Postgres integration * * Capture tracing data for pg. + * + * @deprecated Use `postgresIntegration()` instead. */ export class Postgres extends NodePerformanceIntegration implements Integration { /** @@ -23,6 +47,7 @@ export class Postgres extends NodePerformanceIntegration implements Integr public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Postgres.id; } diff --git a/packages/node-experimental/src/integrations/prisma.ts b/packages/node-experimental/src/integrations/prisma.ts index 203e9d8ed6b1..9edd6ce9d02d 100644 --- a/packages/node-experimental/src/integrations/prisma.ts +++ b/packages/node-experimental/src/integrations/prisma.ts @@ -1,9 +1,27 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { PrismaInstrumentation } from '@prisma/instrumentation'; -import type { Integration } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Integration, IntegrationFn } from '@sentry/types'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; +const _prismaIntegration = (() => { + return { + name: 'Prisma', + setupOnce() { + registerInstrumentations({ + instrumentations: [ + // does not have a hook to adjust spans & add origin + new PrismaInstrumentation({}), + ], + }); + }, + }; +}) satisfies IntegrationFn; + +export const prismaIntegration = defineIntegration(_prismaIntegration); + /** * Prisma integration * @@ -12,6 +30,8 @@ import { NodePerformanceIntegration } from './NodePerformanceIntegration'; * previewFeatures = ["tracing"] * For the prisma client. * See https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing for more details. + * + * @deprecated Use `prismaIntegration()` instead. */ export class Prisma extends NodePerformanceIntegration implements Integration { /** @@ -26,6 +46,7 @@ export class Prisma extends NodePerformanceIntegration implements Integrat public constructor() { super(); + // eslint-disable-next-line deprecation/deprecation this.name = Prisma.id; } diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 8472bcf17d6e..d617845b9e2e 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -18,8 +18,8 @@ import { import { DEBUG_BUILD } from '../debug-build'; import { getAutoPerformanceIntegrations } from '../integrations/getAutoPerformanceIntegrations'; -import { Http } from '../integrations/http'; -import { NodeFetch } from '../integrations/node-fetch'; +import { httpIntegration } from '../integrations/http'; +import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; import { setOpenTelemetryContextAsyncContextStrategy } from '../otel/asyncContextStrategy'; import type { NodeExperimentalClientOptions, NodeExperimentalOptions } from '../types'; import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from './api'; @@ -34,16 +34,16 @@ const ignoredDefaultIntegrations = ['Http', 'Undici']; export const defaultIntegrations: Integration[] = [ // eslint-disable-next-line deprecation/deprecation ...defaultNodeIntegrations.filter(i => !ignoredDefaultIntegrations.includes(i.name)), - new Http(), - new NodeFetch(), + httpIntegration(), + nativeNodeFetchIntegration(), ]; /** Get the default integrations for the Node Experimental SDK. */ export function getDefaultIntegrations(options: Options): Integration[] { return [ ...getDefaultNodeIntegrations(options).filter(i => !ignoredDefaultIntegrations.includes(i.name)), - new Http(), - new NodeFetch(), + httpIntegration(), + nativeNodeFetchIntegration(), ...(hasTracingEnabled(options) ? getAutoPerformanceIntegrations() : []), ]; } diff --git a/packages/node-experimental/src/sdk/spanProcessor.ts b/packages/node-experimental/src/sdk/spanProcessor.ts index 226add7753cf..a85085077e94 100644 --- a/packages/node-experimental/src/sdk/spanProcessor.ts +++ b/packages/node-experimental/src/sdk/spanProcessor.ts @@ -33,12 +33,15 @@ export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { /** @inheritDoc */ protected _shouldSendSpanToSentry(span: Span): boolean { const client = getClient(); + // eslint-disable-next-line deprecation/deprecation const httpIntegration = client ? client.getIntegrationByName('Http') : undefined; + // eslint-disable-next-line deprecation/deprecation const fetchIntegration = client ? client.getIntegrationByName('NodeFetch') : undefined; // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, // So we can generate a breadcrumb for it but no span will be sent + // TODO v8: Remove this if ( (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && span.attributes[SemanticAttributes.HTTP_URL] && diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 929b286452f3..d379070c4ee1 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -540,6 +540,7 @@ describe('Integration | Transactions', () => { if (name === 'Http') { return { shouldCreateSpansForRequests: false, + // eslint-disable-next-line deprecation/deprecation } as Http; } @@ -604,6 +605,7 @@ describe('Integration | Transactions', () => { if (name === 'NodeFetch') { return { shouldCreateSpansForRequests: false, + // eslint-disable-next-line deprecation/deprecation } as NodeFetch; } From 00e7a2e625a72d80a5ad1e2cc80df615ac06c0ed Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 1 Feb 2024 10:18:09 +0100 Subject: [PATCH 24/68] build(ci): Update paths-filter action to v3.0.0 (#10449) Also move off our custom fork as that is no longer needed. This mainly fixes the node16 deprecation warning for the action: https://github.com/dorny/paths-filter/releases/tag/v3.0.0 --- .github/workflows/build.yml | 2 +- .github/workflows/flaky-test-detector.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a46006fe5325..a519965d9212 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,7 +79,7 @@ jobs: echo "COMMIT_MESSAGE=$(git log -n 1 --pretty=format:%s $COMMIT_SHA)" >> $GITHUB_ENV - name: Determine changed packages - uses: getsentry/paths-filter@v2.11.1 + uses: dorny/paths-filter@v3.0.0 id: changed with: filters: | diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 7774ca1d8d0b..6a68dc7278d1 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -71,7 +71,7 @@ jobs: run: npx playwright install-deps - name: Determine changed tests - uses: getsentry/paths-filter@v2.11.1 + uses: dorny/paths-filter@v3.0.0 id: changed with: list-files: json From 9961d29ea8c245691863fef8355ff234b4ec6764 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:36:27 +0100 Subject: [PATCH 25/68] ci(deps): Bump actions/cache from 3 to 4 (#10460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
Release notes

Sourced from actions/cache's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v3...v4.0.0

v3.3.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v3...v3.3.3

v3.3.2

What's Changed

New Contributors

Full Changelog: https://github.com/actions/cache/compare/v3...v3.3.2

v3.3.1

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v3...v3.3.1

v3.3.0

What's Changed

... (truncated)

Changelog

Sourced from actions/cache's changelog.

Releases

3.0.0

  • Updated minimum runner version support from node 12 -> node 16

3.0.1

  • Added support for caching from GHES 3.5.
  • Fixed download issue for files > 2GB during restore.

3.0.2

  • Added support for dynamic cache size cap on GHES.

3.0.3

  • Fixed avoiding empty cache save when no files are available for caching. (issue)

3.0.4

  • Fixed tar creation error while trying to create tar with path as ~/ home folder on ubuntu-latest. (issue)

3.0.5

  • Removed error handling by consuming actions/cache 3.0 toolkit, Now cache server error handling will be done by toolkit. (PR)

3.0.6

  • Fixed #809 - zstd -d: no such file or directory error
  • Fixed #833 - cache doesn't work with github workspace directory

3.0.7

  • Fixed #810 - download stuck issue. A new timeout is introduced in the download process to abort the download if it gets stuck and doesn't finish within an hour.

3.0.8

  • Fix zstd not working for windows on gnu tar in issues #888 and #891.
  • Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable SEGMENT_DOWNLOAD_TIMEOUT_MINS. Default is 60 minutes.

3.0.9

  • Enhanced the warning message for cache unavailablity in case of GHES.

3.0.10

  • Fix a bug with sorting inputs.
  • Update definition for restore-keys in README.md

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-cache/action.yml | 4 ++-- .github/workflows/build.yml | 22 +++++++++++----------- .github/workflows/canary.yml | 4 ++-- .github/workflows/flaky-test-detector.yml | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml index 14fe63bcf69c..848983376840 100644 --- a/.github/actions/restore-cache/action.yml +++ b/.github/actions/restore-cache/action.yml @@ -6,13 +6,13 @@ runs: steps: - name: Check dependency cache id: dep-cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ env.DEPENDENCY_CACHE_KEY }} - name: Check build cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 id: build-cache with: path: ${{ env.CACHED_BUILD_PATHS }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a519965d9212..6c71388f5688 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -183,7 +183,7 @@ jobs: run: echo "hash=${{ hashFiles('yarn.lock', '**/package.json') }}" >> "$GITHUB_OUTPUT" - name: Check dependency cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache_dependencies with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} @@ -227,21 +227,21 @@ jobs: with: node-version-file: 'package.json' - name: Check dependency cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} fail-on-cache-miss: true - name: Check build cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache_built_packages with: path: ${{ env.CACHED_BUILD_PATHS }} key: ${{ env.BUILD_CACHE_KEY }} - name: NX cache - uses: actions/cache@v3 + uses: actions/cache@v4 # Disable cache when: # - on release branches # - when PR has `ci-skip-cache` label or on nightly builds @@ -340,7 +340,7 @@ jobs: with: node-version-file: 'package.json' - name: Check dependency cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} @@ -548,7 +548,7 @@ jobs: - name: Get Playwright version id: playwright-version run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Check if Playwright browser is cached id: playwright-cache with: @@ -642,7 +642,7 @@ jobs: - name: Get Playwright version id: playwright-version run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Check if Playwright browser is cached id: playwright-cache with: @@ -698,7 +698,7 @@ jobs: - name: Get Playwright version id: playwright-version run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Check if Playwright browser is cached id: playwright-cache with: @@ -909,7 +909,7 @@ jobs: env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: NX cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: .nxcache key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }} @@ -918,7 +918,7 @@ jobs: - name: Build tarballs run: yarn build:tarball - name: Stores tarballs in cache - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} @@ -1010,7 +1010,7 @@ jobs: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Restore tarball cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: ${{ github.workspace }}/packages/*/*.tgz key: ${{ env.BUILD_CACHE_TARBALL_KEY }} diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 72dcd46d238d..9a856a7ce034 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -31,7 +31,7 @@ jobs: with: node-version-file: 'package.json' - name: Check canary cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ github.workspace }}/packages/*/*.tgz @@ -100,7 +100,7 @@ jobs: node-version-file: 'package.json' - name: Restore canary cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: | ${{ github.workspace }}/packages/*/*.tgz diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 6a68dc7278d1..d499c12d661b 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -40,7 +40,7 @@ jobs: run: yarn install --ignore-engines --frozen-lockfile - name: NX cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: .nxcache key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }} @@ -55,7 +55,7 @@ jobs: - name: Get Playwright version id: playwright-version run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Check if Playwright browser is cached id: playwright-cache with: From 24401ccae0d7d41bc18f9625c702697920e8dd9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:36:52 +0100 Subject: [PATCH 26/68] ci(deps): Bump codecov/codecov-action from 3 to 4 (#10461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
Release notes

Sourced from codecov/codecov-action's releases.

v4.0.0

v4 of the Codecov Action uses the CLI as the underlying upload. The CLI has helped to power new features including local upload, the global upload token, and new upcoming features.

Breaking Changes

  • The Codecov Action runs as a node20 action due to node16 deprecation. See this post from GitHub on how to migrate.
  • Tokenless uploading is unsupported. However, PRs made from forks to the upstream public repos will support tokenless (e.g. contributors to OS projects do not need the upstream repo's Codecov token). This doc shows instructions on how to add the Codecov token.
  • OS platforms have been added, though some may not be automatically detected. To see a list of platforms, see our CLI download page
  • Various arguments to the Action have been changed. Please be aware that the arguments match with the CLI's needs

v3 versions and below will not have access to CLI features (e.g. global upload token, ATS).

What's Changed

... (truncated)

Changelog

Sourced from codecov/codecov-action's changelog.

4.0.0-beta.2

Fixes

  • #1085 not adding -n if empty to do-upload command

4.0.0-beta.1

v4 represents a move from the universal uploader to the Codecov CLI. Although this will unlock new features for our users, the CLI is not yet at feature parity with the universal uploader.

Breaking Changes

  • No current support for aarch64 and alpine architectures.
  • Tokenless uploading is unsuported
  • Various arguments to the Action have been removed

3.1.4

Fixes

  • #967 Fix typo in README.md
  • #971 fix: add back in working dir
  • #969 fix: CLI option names for uploader

Dependencies

  • #970 build(deps-dev): bump @​types/node from 18.15.12 to 18.16.3
  • #979 build(deps-dev): bump @​types/node from 20.1.0 to 20.1.2
  • #981 build(deps-dev): bump @​types/node from 20.1.2 to 20.1.4

3.1.3

Fixes

  • #960 fix: allow for aarch64 build

Dependencies

  • #957 build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0
  • #958 build(deps): bump openpgp from 5.7.0 to 5.8.0
  • #959 build(deps-dev): bump @​types/node from 18.15.10 to 18.15.12

3.1.2

Fixes

  • #718 Update README.md
  • #851 Remove unsupported path_to_write_report argument
  • #898 codeql-analysis.yml
  • #901 Update README to contain correct information - inputs and negate feature
  • #955 fix: add in all the extra arguments for uploader

Dependencies

  • #819 build(deps): bump openpgp from 5.4.0 to 5.5.0
  • #835 build(deps): bump node-fetch from 3.2.4 to 3.2.10
  • #840 build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4
  • #841 build(deps): bump @​actions/core from 1.9.1 to 1.10.0
  • #843 build(deps): bump @​actions/github from 5.0.3 to 5.1.1
  • #869 build(deps): bump node-fetch from 3.2.10 to 3.3.0
  • #872 build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0
  • #879 build(deps): bump decode-uri-component from 0.2.0 to 0.2.2

... (truncated)

Commits
  • f30e495 fix: update action.yml (#1240)
  • a7b945c fix: allow for other archs (#1239)
  • 98ab2c5 Update package.json (#1238)
  • 43235cc Update README.md (#1237)
  • 0cf8684 chore(ci): bump to node20 (#1236)
  • 8e1e730 build(deps-dev): bump @​typescript-eslint/eslint-plugin from 6.19.1 to 6.20.0 ...
  • 61293af build(deps-dev): bump @​typescript-eslint/parser from 6.19.1 to 6.20.0 (#1235)
  • 7a070cb build(deps): bump github/codeql-action from 3.23.1 to 3.23.2 (#1231)
  • 9097165 build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0 (#1232)
  • ac042ea build(deps-dev): bump @​typescript-eslint/eslint-plugin from 6.19.0 to 6.19.1 ...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c71388f5688..9a60e84e1e79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -425,7 +425,7 @@ jobs: NODE_VERSION: 16 run: yarn test-ci-browser - name: Compute test coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 job_bun_unit_tests: name: Bun Unit Tests @@ -453,7 +453,7 @@ jobs: run: | yarn test-ci-bun - name: Compute test coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 job_deno_unit_tests: name: Deno Unit Tests @@ -486,7 +486,7 @@ jobs: yarn build yarn test - name: Compute test coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 job_node_unit_tests: name: Node (${{ matrix.node }}) Unit Tests @@ -517,7 +517,7 @@ jobs: [[ $NODE_VERSION == 8 ]] && yarn add --dev --ignore-engines --ignore-scripts --ignore-workspace-root-check ts-node@8.10.2 yarn test-ci-node - name: Compute test coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 job_nextjs_integration_test: name: Nextjs (Node ${{ matrix.node }}) Tests From 735672c5fb8e7d390e005cfdedabb91b7df70d6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:39:01 +0100 Subject: [PATCH 27/68] ci(deps): Bump actions/upload-artifact from 4.0.0 to 4.3.0 (#10459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.0.0 to 4.3.0.
Release notes

Sourced from actions/upload-artifact's releases.

v4.3.0

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.3.0

v4.2.0

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.2.0

v4.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v4...v4.1.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4.0.0&new-version=4.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a60e84e1e79..ca46c505789a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -391,7 +391,7 @@ jobs: - name: Pack run: yarn build:tarball - name: Archive artifacts - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.3.0 with: name: ${{ github.sha }} path: | @@ -1116,7 +1116,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} - name: Upload results - uses: actions/upload-artifact@v4.0.0 + uses: actions/upload-artifact@v4.3.0 if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository with: name: ${{ steps.process.outputs.artifactName }} From 17f9b04f30cdc570ce26274779aae222fb02013b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 18:50:23 +0000 Subject: [PATCH 28/68] ci(deps): Bump denoland/setup-deno from 1.1.3 to 1.1.4 (#10462) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca46c505789a..905053d45622 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -473,7 +473,7 @@ jobs: with: node-version-file: 'package.json' - name: Set up Deno - uses: denoland/setup-deno@v1.1.3 + uses: denoland/setup-deno@v1.1.4 with: deno-version: v1.38.5 - name: Restore caches From afa67bfd3ef72dc9d2a619ab064f89a7c8b7c174 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 1 Feb 2024 14:52:15 -0400 Subject: [PATCH 29/68] fix(test): Clean up docker containers with script (#10454) This PR adds a script to cleanup any docker containers left over from the node integration tests. - It uses `globby@11` since that is already installed for a couple of our dependencies. - It finds all the `docker-compose.yml` files and runs `docker compose down --volumes` in those directories. - It catches all errors for when docker is not running --- .../node-integration-tests/package.json | 7 +++++-- .../node-integration-tests/scripts/clean.js | 19 +++++++++++++++++++ yarn.lock | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 dev-packages/node-integration-tests/scripts/clean.js diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 6bad3d7f7a71..4fc394a91a79 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -14,8 +14,8 @@ "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", - "clean": "rimraf -g **/node_modules && run-p clean:docker:*", - "clean:docker:mysql2": "cd suites/tracing-experimental/mysql2 && docker-compose down --volumes", + "clean": "rimraf -g **/node_modules && run-p clean:docker", + "clean:docker": "node scripts/clean.js", "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", "prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)", "lint": "eslint . --format stylish", @@ -52,6 +52,9 @@ "proxy": "^2.1.1", "yargs": "^16.2.0" }, + "devDependencies": { + "globby": "11" + }, "config": { "mongodbMemoryServer": { "preferGlobalPath": true, diff --git a/dev-packages/node-integration-tests/scripts/clean.js b/dev-packages/node-integration-tests/scripts/clean.js new file mode 100644 index 000000000000..0610e39f92d4 --- /dev/null +++ b/dev-packages/node-integration-tests/scripts/clean.js @@ -0,0 +1,19 @@ +const { execSync } = require('child_process'); +const globby = require('globby'); +const { dirname, join } = require('path'); + +const cwd = join(__dirname, '..'); +const paths = globby.sync(['suites/**/docker-compose.yml'], { cwd }).map(path => join(cwd, dirname(path))); + +// eslint-disable-next-line no-console +console.log('Cleaning up docker containers and volumes...'); + +for (const path of paths) { + try { + // eslint-disable-next-line no-console + console.log(`docker compose down @ ${path}`); + execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path }); + } catch (_) { + // + } +} diff --git a/yarn.lock b/yarn.lock index d2946ed22906..8b1623ec39bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16753,7 +16753,7 @@ globby@10.0.0: merge2 "^1.2.3" slash "^3.0.0" -globby@11.1.0, globby@^11.0.1, globby@^11.0.3, globby@^11.1.0: +globby@11, globby@11.1.0, globby@^11.0.1, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== From a489dbf4b1af5b14baab49a33121bb69134bea33 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 1 Feb 2024 16:56:30 -0330 Subject: [PATCH 30/68] fix(feedback): Fix logo color when colorScheme is "system" (#10465) The CSS when colorScheme is "system" was always being overwritten so the logo color ends up being incorrect when OS is in dark mode. ![image](https://github.com/getsentry/sentry-javascript/assets/79684/062b3e1d-d310-470c-93f0-53105cfb89de) --- packages/feedback/src/widget/Logo.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/feedback/src/widget/Logo.ts b/packages/feedback/src/widget/Logo.ts index 17333bda87ed..9e286a970961 100644 --- a/packages/feedback/src/widget/Logo.ts +++ b/packages/feedback/src/widget/Logo.ts @@ -33,8 +33,13 @@ export function Logo({ colorScheme }: Props): IconReturn { const defs = createElementNS('defs'); const style = createElementNS('style'); + style.textContent = ` + path { + fill: ${colorScheme === 'dark' ? '#fff' : '#362d59'}; + }`; + if (colorScheme === 'system') { - style.textContent = ` + style.textContent += ` @media (prefers-color-scheme: dark) { path: { fill: '#fff'; @@ -43,11 +48,6 @@ export function Logo({ colorScheme }: Props): IconReturn { `; } - style.textContent = ` - path { - fill: ${colorScheme === 'dark' ? '#fff' : '#362d59'}; - }`; - defs.append(style); svg.append(defs); From 6a16d0ddbe3931b0b31e038804b9865a2f3a1d2e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 1 Feb 2024 16:57:02 -0330 Subject: [PATCH 31/68] feat(feedback): Add `system-ui` to start of font family (#10464) Requested by @Jesse-Box: "...fits better with the OS the user uses" --- packages/feedback/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/src/constants.ts b/packages/feedback/src/constants.ts index 6e3e9055b511..07782968375f 100644 --- a/packages/feedback/src/constants.ts +++ b/packages/feedback/src/constants.ts @@ -9,7 +9,7 @@ const LIGHT_BACKGROUND = '#ffffff'; const INHERIT = 'inherit'; const SUBMIT_COLOR = 'rgba(108, 95, 199, 1)'; const LIGHT_THEME = { - fontFamily: "'Helvetica Neue', Arial, sans-serif", + fontFamily: "system-ui, 'Helvetica Neue', Arial, sans-serif", fontSize: '14px', background: LIGHT_BACKGROUND, From 4bd82c99f9145e0ec679ac74bbe9f98c5c95a2bd Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 1 Feb 2024 16:23:01 -0500 Subject: [PATCH 32/68] ref(browser-integration-tests): Remove space from replayIntegrationShim (#10467) --- .../{replayIntegrationShim => replayIntegrationShim}/init.js | 0 .../template.html | 0 .../{replayIntegrationShim => replayIntegrationShim}/test.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename dev-packages/browser-integration-tests/suites/replay/{replayIntegrationShim => replayIntegrationShim}/init.js (100%) rename dev-packages/browser-integration-tests/suites/replay/{replayIntegrationShim => replayIntegrationShim}/template.html (100%) rename dev-packages/browser-integration-tests/suites/replay/{replayIntegrationShim => replayIntegrationShim}/test.ts (100%) diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js rename to dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html rename to dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/template.html diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts rename to dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts From e6597b9450b4d527b79d1792624c0404cee64107 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 1 Feb 2024 21:15:56 -0500 Subject: [PATCH 33/68] pkg(profiling-node): port profiling-node repo to monorepo (#10151) Ports profiling-node to monorepo This should only require CI step changes and no changes to the source code (except inheriting from base configs in monorepo, but those were already ported to profiling-node so they shouldn't result in any actual changes to the codebase). TODO: - [x] verify pkg cmds work and we have no lint/ts issues. - [x] verify tests pass - [x] verify ci commands work - [x] prebuild binaries - [x] port build to rollup - [x] create e2e verdaccio config - [x] ensure e2e tests pass and the app can build. I'm opening this as ready to review. Currently e2e test on profiling fails as the package is missing. I'm not exactly sure why that is so hoping I can get a helping hand on that. The condition for a successful e2e test is to just execute a node script which triggers a profile and attempt to bundle profiling-node. The bundler test is there to ensure that we do in fact provide all of the statically required prebuild binaries and ensure bundlers can resolve them - else we risk breaking build tooling for folks bundling their code, which can be a common optimization in serverless environments. The good: - Node profiling can resolve some issues around types which kept falling out fo sync with the SDK - We can follow the changes in JS SDK and core packages along as well as their versioning - The integration between the rest of the packages is tighter and safer, as it should be impossible for the sentry-javascript packages we rely on now to break in profiling-node. The unfortunate: - CI timings will increase mostly because of the performance of windows runners. Installing repository dependencies on there takes roughly 10 minutes. I have tried leveraging the cache as much as I could, but since our dep paths are marked as `~/path/to/cached_dep` and `${{github.workspace}}/path/to/dep` it means they cannot be cached cross os as paths differ. I did not change those paths in this PR, but it can be an optimization as I suspect most of the packages are pure js and nothing would break. - When we pack artifacts in yarn build:tarball, we need skip profiling-node and assemble it separately. This is because we need to pull in the prebuilt binaries, else we'll only have the packed binary for the os we are running the action on. This same step needs to be replicated in e2e tests which prepare the tarball as well. Couple of things I had to fix: - Our build tooling was not windows compatible (util polyfill was using awrong os path separator which wound up creating an incompatible build output on windows) - CI is finicky. I learned the hard way that node-gyp configure and build need to be sequential, else all hell breaks loose and for reasons which I didn't bother to investigate, python path and tooling is never correctly resolved (even when specified via gyp arg) --- .craft.yml | 3 + .github/workflows/build.yml | 334 ++++- biome.json | 3 +- .../node-profiling/.gitignore | 1 + .../test-applications/node-profiling/.npmrc | 2 + .../node-profiling/build.mjs | 19 + .../test-applications/node-profiling/index.js | 18 + .../node-profiling/package.json | 21 + .../e2e-tests/verdaccio-config/config.yaml | 6 + .../plugins/extractPolyfillsPlugin.mjs | 6 +- package.json | 4 +- packages/integrations/scripts/buildBundles.ts | 1 + packages/profiling-node/.eslintignore | 4 + packages/profiling-node/.eslintrc.js | 14 + packages/profiling-node/.gitignore | 6 + packages/profiling-node/CHANGES | 505 ++++++++ packages/profiling-node/LICENSE | 21 + packages/profiling-node/README.md | 301 +++++ packages/profiling-node/binding.gyp | 10 + .../profiling-node/bindings/cpu_profiler.cc | 1118 +++++++++++++++++ packages/profiling-node/clang-format.js | 20 + packages/profiling-node/jest.config.js | 6 + packages/profiling-node/package.json | 96 ++ packages/profiling-node/rollup.npm.config.mjs | 25 + packages/profiling-node/scripts/binaries.js | 27 + .../profiling-node/scripts/check-build.js | 56 + .../profiling-node/scripts/copy-target.js | 27 + .../scripts/prune-profiler-binaries.js | 189 +++ packages/profiling-node/src/cpu_profiler.ts | 154 +++ packages/profiling-node/src/debug-build.ts | 8 + packages/profiling-node/src/hubextensions.ts | 253 ++++ packages/profiling-node/src/index.ts | 1 + packages/profiling-node/src/integration.ts | 245 ++++ packages/profiling-node/src/types.ts | 105 ++ packages/profiling-node/src/utils.ts | 513 ++++++++ packages/profiling-node/test/bindings.test.ts | 30 + .../profiling-node/test/cpu_profiler.test.ts | 302 +++++ .../test/hubextensions.hub.test.ts | 481 +++++++ .../profiling-node/test/hubextensions.test.ts | 242 ++++ packages/profiling-node/test/index.test.ts | 166 +++ .../profiling-node/test/integration.test.ts | 272 ++++ packages/profiling-node/test/utils.test.ts | 361 ++++++ packages/profiling-node/tsconfig.json | 11 + packages/profiling-node/tsconfig.test.json | 12 + packages/profiling-node/tsconfig.types.json | 11 + scripts/node-unit-tests.ts | 1 + yarn.lock | 107 +- 47 files changed, 6107 insertions(+), 11 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/build.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/index.js create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/package.json create mode 100644 packages/profiling-node/.eslintignore create mode 100644 packages/profiling-node/.eslintrc.js create mode 100644 packages/profiling-node/.gitignore create mode 100644 packages/profiling-node/CHANGES create mode 100644 packages/profiling-node/LICENSE create mode 100644 packages/profiling-node/README.md create mode 100644 packages/profiling-node/binding.gyp create mode 100644 packages/profiling-node/bindings/cpu_profiler.cc create mode 100644 packages/profiling-node/clang-format.js create mode 100644 packages/profiling-node/jest.config.js create mode 100644 packages/profiling-node/package.json create mode 100644 packages/profiling-node/rollup.npm.config.mjs create mode 100644 packages/profiling-node/scripts/binaries.js create mode 100644 packages/profiling-node/scripts/check-build.js create mode 100644 packages/profiling-node/scripts/copy-target.js create mode 100755 packages/profiling-node/scripts/prune-profiler-binaries.js create mode 100644 packages/profiling-node/src/cpu_profiler.ts create mode 100644 packages/profiling-node/src/debug-build.ts create mode 100644 packages/profiling-node/src/hubextensions.ts create mode 100644 packages/profiling-node/src/index.ts create mode 100644 packages/profiling-node/src/integration.ts create mode 100644 packages/profiling-node/src/types.ts create mode 100644 packages/profiling-node/src/utils.ts create mode 100644 packages/profiling-node/test/bindings.test.ts create mode 100644 packages/profiling-node/test/cpu_profiler.test.ts create mode 100644 packages/profiling-node/test/hubextensions.hub.test.ts create mode 100644 packages/profiling-node/test/hubextensions.test.ts create mode 100644 packages/profiling-node/test/index.test.ts create mode 100644 packages/profiling-node/test/integration.test.ts create mode 100644 packages/profiling-node/test/utils.test.ts create mode 100644 packages/profiling-node/tsconfig.json create mode 100644 packages/profiling-node/tsconfig.test.json create mode 100644 packages/profiling-node/tsconfig.types.json diff --git a/.craft.yml b/.craft.yml index 7c09cb4ddd4c..99efaf3d095e 100644 --- a/.craft.yml +++ b/.craft.yml @@ -44,6 +44,9 @@ targets: - name: npm id: '@sentry/node' includeNames: /^sentry-node-\d.*\.tgz$/ + - name: npm + id: '@sentry/profiling-node' + includeNames: /^sentry-profiling-node-\d.*\.tgz$/ ## 3 Browser-based Packages - name: npm diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 905053d45622..74ccf23dc669 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,8 @@ concurrency: env: HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }} + # WARNING: this disables cross os caching as ~ and + # github.workspace evaluate to differents paths CACHED_DEPENDENCY_PATHS: | ${{ github.workspace }}/node_modules ${{ github.workspace }}/packages/*/node_modules @@ -33,6 +35,8 @@ env: # DEPENDENCY_CACHE_KEY: can't be set here because we don't have access to yarn.lock + # WARNING: this disables cross os caching as ~ and + # github.workspace evaluate to differents paths # packages/utils/cjs and packages/utils/esm: Symlinks to the folders inside of `build`, needed for tests CACHED_BUILD_PATHS: | ${{ github.workspace }}/dev-packages/*/build @@ -127,7 +131,12 @@ jobs: - *shared - 'packages/node/**' - 'packages/node-experimental/**' + - 'packages/profiling-node/**' - 'dev-packages/node-integration-tests/**' + profiling_node: + - *shared + - 'packages/node/**' + - 'packages/profiling-node/**' deno: - *shared - *browser @@ -145,6 +154,7 @@ jobs: changed_ember: ${{ steps.changed.outputs.ember }} changed_remix: ${{ steps.changed.outputs.remix }} changed_node: ${{ steps.changed.outputs.node }} + changed_profiling_node: ${{ steps.changed.outputs.profiling_node }} changed_deno: ${{ steps.changed.outputs.deno }} changed_browser: ${{ steps.changed.outputs.browser }} changed_browser_integration: ${{ steps.changed.outputs.browser_integration }} @@ -322,6 +332,8 @@ jobs: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Lint source files run: yarn lint:lerna + - name: Lint C++ files + run: yarn lint:clang - name: Validate ES5 builds run: yarn validate:es5 @@ -371,7 +383,7 @@ jobs: job_artifacts: name: Upload Artifacts - needs: [job_get_metadata, job_build] + needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] runs-on: ubuntu-20.04 # Build artifacts are only needed for releasing workflow. if: needs.job_get_metadata.outputs.is_release == 'true' @@ -388,8 +400,16 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Pack - run: yarn build:tarball + - name: Profiling + # Profiling tarball is built separately as we assemble the precompiled binaries + run: yarn build:tarball --ignore @sentry/profiling-node + + - name: Restore profiling tarball + uses: actions/download-artifact@v3 + with: + name: profiling-node-tarball-${{ github.sha }} + path: packages/profiling-node + - name: Archive artifacts uses: actions/upload-artifact@v4.3.0 with: @@ -519,6 +539,32 @@ jobs: - name: Compute test coverage uses: codecov/codecov-action@v4 + job_profiling_node_unit_tests: + name: Node Profiling Unit Tests + needs: [job_get_metadata, job_build] + if: needs.job_get_metadata.outputs.changed_node || needs.job_get_metadata.outputs.changed_profiling_node == 'true' || github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out current commit + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_COMMIT }} + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: actions/setup-python@v5 + - name: Restore caches + uses: ./.github/actions/restore-cache + env: + DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Build Configure node-gyp + run: yarn lerna run build:bindings:configure --scope @sentry/profiling-node + - name: Build Bindings for Current Environment + run: yarn build --scope @sentry/profiling-node + - name: Unit Test + run: yarn lerna run test --scope @sentry/profiling-node + job_nextjs_integration_test: name: Nextjs (Node ${{ matrix.node }}) Tests needs: [job_get_metadata, job_build] @@ -892,7 +938,7 @@ jobs: if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' - needs: [job_get_metadata, job_build] + needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] runs-on: ubuntu-20.04-large-js timeout-minutes: 15 steps: @@ -916,7 +962,24 @@ jobs: # On develop branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it restore-keys: ${{ env.NX_CACHE_RESTORE_KEYS }} - name: Build tarballs - run: yarn build:tarball + run: yarn build:tarball --ignore @sentry/profiling-node + + # Rebuild profiling by compiling TS and pulling the precompiled binaries + - name: Build Profiling Node + run: yarn lerna run build:lib --scope @sentry/profiling-node + + - name: Extract Profiling Node Prebuilt Binaries + # @TODO: v4 breaks convenient merging of same name artifacts + # https://github.com/actions/upload-artifact/issues/478 + uses: actions/download-artifact@v3 + with: + name: profiling-node-binaries-${{ github.sha }} + path: ${{ github.workspace }}/packages/profiling-node/lib/ + + - name: Build Profiling tarball + run: yarn build:tarball --scope @sentry/profiling-node + # End rebuild profiling + - name: Stores tarballs in cache uses: actions/cache/save@v4 with: @@ -968,6 +1031,7 @@ jobs: 'node-experimental-fastify-app', 'node-hapi-app', 'node-exports-test-app', + 'node-profiling' ] build-command: - false @@ -1009,6 +1073,27 @@ jobs: env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + # Rebuild profiling by compiling TS and pulling the precompiled binaries + - name: Build Profiling Node + run: yarn lerna run build:lib --scope @sentry/profiling-node + + - name: Extract Profiling Node Prebuilt Binaries + # @TODO: v4 breaks convenient merging of same name artifacts + # https://github.com/actions/upload-artifact/issues/478 + uses: actions/download-artifact@v3 + with: + name: profiling-node-binaries-${{ github.sha }} + path: ${{ github.workspace }}/packages/profiling-node/lib/ + + - name: Build Profiling tarball + run: yarn build:tarball --scope @sentry/profiling-node + + - name: Install esbuild + if: ${{ matrix.test-application == 'node-profiling' }} + working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + run: yarn add esbuild@0.19.11 + # End rebuild profiling + - name: Restore tarball cache uses: actions/cache/restore@v4 with: @@ -1055,11 +1140,13 @@ jobs: needs: [ job_build, + job_compile_bindings_profiling_node, job_browser_build_tests, job_browser_unit_tests, job_bun_unit_tests, job_deno_unit_tests, job_node_unit_tests, + job_profiling_node_unit_tests, job_nextjs_integration_test, job_node_integration_tests, job_browser_playwright_tests, @@ -1121,3 +1208,240 @@ jobs: with: name: ${{ steps.process.outputs.artifactName }} path: ${{ steps.process.outputs.artifactPath }} + + job_compile_bindings_profiling_node: + name: Compile & Test Profiling Bindings (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.node || matrix.container }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} + needs: [job_get_metadata, job_install_deps, job_build] + # Compiling bindings can be very slow (especially on windows), so only run precompile + # if profiling or profiling node package had changed or if we are on a release branch. + if: | + (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || + (needs.job_get_metadata.outputs.is_release) || + (github.event_name != 'pull_request') + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + # x64 glibc + - os: ubuntu-20.04 + node: 16 + - os: ubuntu-20.04 + node: 18 + - os: ubuntu-20.04 + node: 20 + + # x64 musl + - os: ubuntu-20.04 + container: node:16-alpine3.16 + node: 16 + - os: ubuntu-20.04 + container: node:18-alpine3.17 + node: 18 + - os: ubuntu-20.04 + container: node:20-alpine3.17 + node: 20 + + # arm64 glibc + - os: ubuntu-20.04 + arch: arm64 + node: 16 + - os: ubuntu-20.04 + arch: arm64 + node: 18 + - os: ubuntu-20.04 + arch: arm64 + node: 20 + + # arm64 musl + - os: ubuntu-20.04 + container: node:16-alpine3.16 + arch: arm64 + node: 16 + - os: ubuntu-20.04 + arch: arm64 + container: node:18-alpine3.17 + node: 18 + - os: ubuntu-20.04 + arch: arm64 + container: node:20-alpine3.17 + node: 20 + + # macos x64 + - os: macos-11 + node: 16 + arch: x64 + - os: macos-11 + node: 18 + arch: x64 + - os: macos-11 + node: 20 + arch: x64 + + # macos arm64 + - os: macos-12 + arch: arm64 + node: 16 + target_platform: darwin + + - os: macos-12 + arch: arm64 + node: 18 + target_platform: darwin + + - os: macos-12 + arch: arm64 + node: 20 + target_platform: darwin + + # windows x64 + - os: windows-2022 + node: 16 + arch: x64 + + - os: windows-2022 + node: 18 + arch: x64 + + - os: windows-2022 + node: 20 + arch: x64 + steps: + - name: Setup (alpine) + if: contains(matrix.container, 'alpine') + run: | + apk add --no-cache build-base git g++ make curl python3 + ln -sf python3 /usr/bin/python + + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v2 + with: + ref: ${{ env.HEAD_COMMIT }} + + - name: Restore dependency cache + uses: actions/cache/restore@v3 + id: restore-dependencies + with: + path: ${{ env.CACHED_DEPENDENCY_PATHS }} + key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + enableCrossOsArchive: true + + - name: Restore build cache + uses: actions/cache/restore@v3 + id: restore-build + with: + path: ${{ env.CACHED_BUILD_PATHS }} + key: ${{ needs.job_build.outputs.dependency_cache_key }} + enableCrossOsArchive: true + + - name: Configure safe directory + run: | + git config --global --add safe.directory "*" + + - name: Install yarn + run: npm i -g yarn@1.22.19 --force + + - name: Increase yarn network timeout on Windows + if: contains(matrix.os, 'windows') + run: yarn config set network-timeout 600000 -g + + - name: Setup python + uses: actions/setup-python@v4 + if: ${{ !contains(matrix.container, 'alpine') }} + id: python-setup + with: + python-version: '3.8.10' + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Install Dependencies + if: steps.restore-dependencies.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --ignore-engines --ignore-scripts + + - name: Setup (arm64| ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' + run: | + sudo apt-get update + sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + + - name: Setup Musl + if: contains(matrix.container, 'alpine') + run: | + cd packages/profiling-node + curl -OL https://musl.cc/aarch64-linux-musl-cross.tgz + tar -xzvf aarch64-linux-musl-cross.tgz + $(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc --version + + # configure node-gyp + - name: Configure node-gyp + if: matrix.arch != 'arm64' + run: | + cd packages/profiling-node + yarn build:bindings:configure + + - name: Configure node-gyp (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && matrix.target_platform != 'darwin' + run: | + cd packages/profiling-node + yarn build:bindings:configure:arm64 + + - name: Configure node-gyp (arm64, darwin) + if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin' + run: | + cd packages/profiling-node + yarn build:bindings:configure:arm64 + + # build bindings + - name: Build Bindings + if: matrix.arch != 'arm64' + run: | + yarn lerna run build:bindings --scope @sentry/profiling-node + + - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' + run: | + cd packages/profiling-node + CC=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc \ + CXX=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-g++ \ + BUILD_ARCH=arm64 \ + yarn build:bindings + + - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' + run: | + cd packages/profiling-node + CC=aarch64-linux-gnu-gcc \ + CXX=aarch64-linux-gnu-g++ \ + BUILD_ARCH=arm64 \ + yarn build:bindings:arm64 + + - name: Build Bindings (arm64, darwin) + if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin' + run: | + cd packages/profiling-node + BUILD_PLATFORM=darwin \ + BUILD_ARCH=arm64 \ + yarn build:bindings:arm64 + + - name: Build Monorepo + if: steps.restore-build.outputs.cache-hit != 'true' + run: yarn build --scope @sentry/profiling-node + + - name: Test Bindings + if: matrix.arch != 'arm64' + run: | + yarn lerna run test --scope @sentry/profiling-node + + - name: Archive Binary + # @TODO: v4 breaks convenient merging of same name artifacts + # https://github.com/actions/upload-artifact/issues/478 + uses: actions/upload-artifact@v3 + with: + name: profiling-node-binaries-${{ github.sha }} + path: | + ${{ github.workspace }}/packages/profiling-node/lib/*.node diff --git a/biome.json b/biome.json index 3fc89f8a7cd3..c18c0720b6d1 100644 --- a/biome.json +++ b/biome.json @@ -64,7 +64,8 @@ ".next/**", ".svelte-kit/**", ".angular/**", - "angular.json" + "angular.json", + "**/profiling-node/lib/**" ] }, "javascript": { diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/.gitignore b/dev-packages/e2e-tests/test-applications/node-profiling/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/.npmrc b/dev-packages/e2e-tests/test-applications/node-profiling/.npmrc new file mode 100644 index 000000000000..949fbddc2343 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/.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-profiling/build.mjs b/dev-packages/e2e-tests/test-applications/node-profiling/build.mjs new file mode 100644 index 000000000000..cdf744355fe8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/build.mjs @@ -0,0 +1,19 @@ +// Because bundlers can now predetermine a static set of binaries we need to ensure those binaries +// actually exists, else we risk a compile time error when bundling the package. This could happen +// if we added a new binary in cpu_profiler.ts, but forgot to prebuild binaries for it. Because CI +// only runs integration and unit tests, this change would be missed and could end up in a release. +// Therefor, once all binaries are precompiled in CI and tests pass, run esbuild with bundle:true +// which will copy all binaries to the outfile folder and throw if any of them are missing. +import esbuild from 'esbuild'; + +console.log('Running build using esbuild version', esbuild.version); + +esbuild.buildSync({ + platform: 'node', + entryPoints: ['./index.js'], + outdir: './dist', + target: 'esnext', + format: 'cjs', + bundle: true, + loader: { '.node': 'copy' }, +}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.js b/dev-packages/e2e-tests/test-applications/node-profiling/index.js new file mode 100644 index 000000000000..bd440f4f17be --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); +const Profiling = require('@sentry/profiling-node'); + +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + +Sentry.init({ + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [new Profiling.ProfilingIntegration()], + tracesSampleRate: 1.0, + profilesSampleRate: 1.0, +}); + +const txn = Sentry.startTransaction('Precompile test'); + +(async () => { + await wait(500); + txn.finish(); +})(); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json new file mode 100644 index 000000000000..8d2bfff693eb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json @@ -0,0 +1,21 @@ +{ + "name": "node-profiling", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "node build.mjs", + "start": "node index.js", + "test": "node index.js && node build.mjs", + "clean": "npx rimraf node_modules", + "test:build": "npm run build", + "test:assert": "npm run test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/profiling-node": "latest || *" + }, + "devDependencies": {}, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 0f1fdee05669..2ed138f1cdcc 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -128,6 +128,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/profiling-node': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/react': access: $all publish: $all diff --git a/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs b/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs index 0acbb175ebf8..ca5ff99438fd 100644 --- a/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs +++ b/dev-packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs @@ -42,7 +42,7 @@ export function makeExtractPolyfillsPlugin() { // The index.js file of the tuils package will include identifiers named after polyfills so we would inject the // polyfills, however that would override the exports so we should just skip that file. - const isUtilsPackage = process.cwd().endsWith('packages/utils'); + const isUtilsPackage = process.cwd().endsWith(`packages${path.sep}utils`); if (isUtilsPackage && sourceFile === 'index.js') { return null; } @@ -194,7 +194,9 @@ function createImportOrRequireNode(polyfillNodes, currentSourceFile, moduleForma // relative const isUtilsPackage = process.cwd().endsWith(path.join('packages', 'utils')); const importSource = literal( - isUtilsPackage ? `./${path.relative(path.dirname(currentSourceFile), 'buildPolyfills')}` : '@sentry/utils', + isUtilsPackage + ? `.${path.sep}${path.relative(path.dirname(currentSourceFile), 'buildPolyfills')}` + : '@sentry/utils', ); // This is the `x, y, z` of inside of `import { x, y, z }` or `var { x, y, z }` diff --git a/package.json b/package.json index 9a226b3d8408..9ecacd34c252 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "changelog": "ts-node ./scripts/get-commit-list.ts", "link:yarn": "lerna exec yarn link", "lint": "run-s lint:lerna lint:biome lint:prettier", + "lint:clang": "lerna run lint:clang", "lint:lerna": "lerna run lint", "lint:biome": "biome check .", "lint:prettier": "prettier **/*.md *.md **/*.css --check", @@ -33,7 +34,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,opentelemetry-node,serverless,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,node-experimental,opentelemetry-node,profiling-node,serverless,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", @@ -64,6 +65,7 @@ "packages/node-experimental", "packages/opentelemetry-node", "packages/opentelemetry", + "packages/profiling-node", "packages/react", "packages/remix", "packages/replay", diff --git a/packages/integrations/scripts/buildBundles.ts b/packages/integrations/scripts/buildBundles.ts index b5eb77730d40..97730f10afe2 100644 --- a/packages/integrations/scripts/buildBundles.ts +++ b/packages/integrations/scripts/buildBundles.ts @@ -17,6 +17,7 @@ function getIntegrations(): string[] { async function buildBundle(integration: string, jsVersion: string): Promise { return new Promise((resolve, reject) => { const child = spawn('yarn', ['--silent', 'rollup', '--config', 'rollup.bundle.config.mjs'], { + shell: true, // required to run on Windows env: { ...process.env, INTEGRATION_FILE: integration, JS_VERSION: jsVersion }, }); diff --git a/packages/profiling-node/.eslintignore b/packages/profiling-node/.eslintignore new file mode 100644 index 000000000000..0deb19641d74 --- /dev/null +++ b/packages/profiling-node/.eslintignore @@ -0,0 +1,4 @@ +node_modules/ +build/ +lib/ +coverage/ diff --git a/packages/profiling-node/.eslintrc.js b/packages/profiling-node/.eslintrc.js new file mode 100644 index 000000000000..84ad1f9e91b7 --- /dev/null +++ b/packages/profiling-node/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + + ignorePatterns: ['lib/**/*', 'examples/**/*', 'jest.co'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + '@sentry-internal/sdk/no-nullish-coalescing': 'off', + '@sentry-internal/sdk/no-unsupported-es6-methods': 'off', + '@sentry-internal/sdk/no-class-field-initializers': 'off', + }, +}; diff --git a/packages/profiling-node/.gitignore b/packages/profiling-node/.gitignore new file mode 100644 index 000000000000..7a329e70a46c --- /dev/null +++ b/packages/profiling-node/.gitignore @@ -0,0 +1,6 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/node_modules/ +/lib/ + diff --git a/packages/profiling-node/CHANGES b/packages/profiling-node/CHANGES new file mode 100644 index 000000000000..012426c29a2f --- /dev/null +++ b/packages/profiling-node/CHANGES @@ -0,0 +1,505 @@ +## This is an old changelog. + +The profiling-node package has since been migrated to sentry-javascript monorepo. Any changes to this package made after +the migration are now tracked in the root CHANGELOG.md. + +## 1.3.3 + +### Various fixes & improvements + +- ci: make clang-format error when formatting errors are raised. (#230) by @JonasBa +- fix: cleanup timer and reuse exports (#229) by @JonasBa + +## 1.3.2 + +### Various fixes & improvements + +- deps(detect-libc): detect-libc is required for install scripts (#226) by @JonasBa + +## 1.3.1 + +### Various fixes & improvements + +- fix(profiling): add node-abi (d3b3ea9c) by @JonasBa + +## 1.3.0 + +### Various fixes & improvements + +- fix(profiling): node-gyp missing python (#223) by @JonasBa +- fix: change package.json keys (#222) by @anonrig +- test: move tests to dedicated test folder (#221) by @JonasBa +- chore: remove prettier (#220) by @anonrig +- format: add clang format (#214) by @JonasBa +- ref(deps) move all deps to dev deps (#218) by @JonasBa +- docs: add rollup external config section (#216) by @JonasBa +- deps: update to 7.85 (#215) by @JonasBa +- perf: avoid deep string copy (#213) by @anonrig +- chore: Add vite external to profiling node instructions (#209) by @AbhiPrasad + +## 1.2.6 + +### Various fixes & improvements + +- fix: check inf and remove rounding (#208) by @JonasBa +- fix(isnan): set rate to 0 if its nan (#207) by @JonasBa + +## 1.2.5 + +### Various fixes & improvements + +- fix(profiling): cap double precision (#206) by @JonasBa + +## 1.2.4 + +### Various fixes & improvements + +- fix(measurements): guard from negative cpu usage (c7ebac41) by @JonasBa + +## 1.2.3 + +### Various fixes & improvements + +- fix(profiling): if count is 0 dont serialize measurements (#205) by @JonasBa + +## 1.2.2 + +### Various fixes & improvements + +- deps(sentry): bump sentry deps (#203) by @sanjaytwisk +- build(deps-dev): bump @babel/traverse from 7.22.20 to 7.23.2 (#202) by @dependabot +- feat(preprocessEvent): emit preprocessEvent for profiles (#198) by @JonasBa +- Update README.md to include Next.js 13+ bundling (#200) by @Negan1911 + +## 1.2.1 + +### Various fixes & improvements + +- fix: dont throw if profiler returns nulptr (#197) by @JonasBa + +## 1.2.0 + +### Various fixes & improvements + +- fix(build): catch spawn err (#193) by @JonasBa +- fix(build): cross compile from x64 to arm (#194) by @JonasBa +- feat(measurements): collect heap usage (#187) by @JonasBa + +## 1.1.3 + +### Various fixes & improvements + +- deps(sentry): bump sentry deps (#191) by @JonasBa +- ci: test app build and run before publish (#183) by @JonasBa +- build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 (#185) by @dependabot +- fix(types): correct frame type (cc74b6e1) by @JonasBa + +## 1.1.2 + +### Various fixes & improvements + +- fix: revert output of types to single file (#182) by @JonasBa + +## 1.1.1 + +### Various fixes & improvements + +- fix: setup musl (34874a63) by @JonasBa +- fix: attempt to recompile on all errors (#180) by @JonasBa +- fix:rebuild binary on any error (#179) by @JonasBa + +## 1.1.0 + +### Various fixes & improvements + +- bindings: prebuild more binaries for node 20 (#177) by @JonasBa +- build(deps): bump semver from 6.3.0 to 6.3.1 (#175) by @dependabot +- ref(profiling): change import so contextReplacementPlugin can ignore warning and attempt to provide darwin binaries + (#176) by @JonasBa + +## 1.0.9 + +### Various fixes & improvements + +- fix(require): require is no longer async (#174) by @JonasBa + +## 1.0.8 + +### Various fixes & improvements + +- fix: use gnu aarch compiler for arm64 binary (#172) by @bohdanw2 +- fix: Issue fixed for building binary with recompileFromSource (#173) by @whaagmans + +## 1.0.7 + +### Various fixes & improvements + +- fix(build): overwrited dest target (b741891d) by @JonasBa +- fix(build): run as single spawn cmd and fail if target already exists (#169) by @JonasBa +- build: stop error handling and just propagate all errors (#167) by @JonasBa +- build: fix typo (9dbc32fb) by @JonasBa +- build: just dont handle errors (ee0f9b95) by @JonasBa +- build: improve recompile error handling (07f2fd4d) by @JonasBa + +## 1.0.6 + +### Various fixes & improvements + +- build: drop exports entirely (28db74c6) by @JonasBa + +## 1.0.5 + +### Various fixes & improvements + +- build: drop esm support (#166) by @JonasBa + +## 1.0.4 + +### Various fixes & improvements + +- fix: check compile on install instead of postinstall (#163) by @JonasBa + +## 1.0.3 + +### Various fixes & improvements + +- Revert "fix: require instead of import in esm (#162)" (3b2f77fb) by @JonasBa + +## 1.0.2 + +### Various fixes & improvements + +- fix: require instead of import in esm (#162) by @JonasBa + +## 1.0.1 + +### Various fixes & improvements + +- fix: polyfill to level createRequire for esm (#161) by @JonasBa + +## 1.0.0 + +- No documented changes. + +## 1.0.0-beta.2 + +### Various fixes & improvements + +- fix: remove esm polyfills (#159) by @JonasBa + +## 1.0.0-beta.1 + +### Various fixes & improvements + +- build: run update before installing tooling to fix stale index (e4c0916e) by @JonasBa +- fix broken reference to copy-target script (#156) by @alekitto +- fix(profiling): remove app_root relative dir detection (#155) by @JonasBa +- fix(profiling): fix build banner typo (47da6797) by @JonasBa +- readme: add prune docs (9e4a0f3f) by @JonasBa + +## 1.0.0-alpha.7 + +### Various fixes & improvements + +- ref(tracing): drop @sentry/tracing (#153) by @JonasBa +- fix(esm): fix esm compile error (1c4a3cc6) by @JonasBa +- deps(tracing): remove tracing dependency (#152) by @JonasBa +- feat(scripts): introduce a cleanup script (#151) by @JonasBa +- feat(profiling): debug_id support (#144) by @JonasBa + +## 1.0.0-alpha.6 + +### Various fixes & improvements + +- readme: drop beta mentions (c3c66a72) by @JonasBa +- ci: test on node20 (d33110de) by @JonasBa +- ref: remove options options type (41b8544f) by @JonasBa +- ref: use options type (154255d3) by @JonasBa +- fix: segfault in node18 (95545180) by @JonasBa +- fix identifier (21425e4f) by @JonasBa +- rename to mjs (a4d50996) by @JonasBa +- fallthrough in switch (e9f6a872) by @JonasBa +- fix: add back return type (d7560395) by @JonasBa +- fix: profiling binary fallthrough (beaf0c0a) by @JonasBa +- fix: build needs require (bc39eaab) by @JonasBa +- fix(build): enumerate precompiled binaries (#146) by @JonasBa +- feat(profiling): bundle lib code (#145) by @JonasBa +- fix(profiling): add exports to package.json (#142) by @JonasBa +- perf: optimize string ops + remove nan (#140) by @JonasBa +- ref: remove profile context before sending (#138) by @JonasBa + +## 1.0.0-alpha.5 + +### Various fixes & improvements + +- fix: use format version (0850fa0c) by @JonasBa + +## 1.0.0-alpha.4 + +### Various fixes & improvements + +- fix(sdk): bump sdk version and read it (#137) by @JonasBa + +## 1.0.0-alpha.3 + +### Various fixes & improvements + +- fix(frames): fix frame attributes (afc1c7b0) by @JonasBa + +## 1.0.0-alpha.2 + +### Various fixes & improvements + +- feat(esm): build esm properly (#135) by @JonasBa + +## 1.0.0-alpha.1 + +### Various fixes & improvements + +- fix(ci): unpack binaries to lib/binaries (5bbf957f) by @JonasBa +- gh: fix label for install issue (d0602b40) by @JonasBa +- gh: fix label for install issue (ca82e73f) by @JonasBa +- gh: add installation issue template (d1f0a304) by @JonasBa +- ci: downgrade to ubuntu 20.04 (8364deba) by @JonasBa +- fix(status): inline status assertions (#133) by @JonasBa +- perf: use a module cache (#131) by @JonasBa +- ci: bump and pin node images (#130) by @JonasBa +- feat(module): parse module (#129) by @JonasBa +- fix(precompile): fix dir sync (#128) by @JonasBa +- ref: remove test log (c7529b14) by @JonasBa +- ref(profiling): drop nan for node abi (#127) by @JonasBa +- feat(build): output esm and cjs modules (#126) by @JonasBa +- Check if module exists before loading. Compile if missing (#122) by @vidhu +- Add @sentry/core as dep (#125) by @vidhu + +## 0.3.0 + +### Various fixes & improvements + +- fix(profiling): avoid unnecessary copy operations (#117) by @JonasBa +- feat(profiling): expose timeout experiment and handle timeout in hooks (#118) by @JonasBa +- ref(profiling): add SDK hooks support (#110) by @JonasBa +- build(deps-dev): bump sqlite3 from 5.1.2 to 5.1.5 (#111) by @dependabot +- docs: update install link for sentry profiling (#116) by @emilsivervik +- feat(profiling): only mark in_app: false for system code (#114) by @JonasBa +- ref(format): remove format macros (cee68c53) by @JonasBa +- ref(prebuilds): remove binary (6ff35ecb) by @JonasBa +- feat(profiling): add profilesSampler (#109) by @JonasBa +- ref(format): remove transactions array in favor of transaction property (#108) by @JonasBa +- chore: improve documentation around prebuild binaries (dc37cbfb) by @JonasBa + +## 0.2.2 + +### Various fixes & improvements + +- deps(sentry): bump sentry packages (22610c47) by @JonasBa + +## 0.2.1 + +### Various fixes & improvements + +- Update README.md (a1e128c2) by @JonasBa +- Update README.md (d0ad2379) by @JonasBa +- Update README.md (d97aad6a) by @JonasBa +- fix(env): read env from sdk (#103) by @JonasBa + +## 0.2.0 + +### Various fixes & improvements + +- feat(profiling): switch to eager logging by default (#102) by @JonasBa +- Update README.md (2d8fd065) by @JonasBa +- build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 (#100) by @dependabot + +## 0.1.0 + +- No documented changes. + +## 0.1.0-alpha.2 + +### Various fixes & improvements + +- fix(bin): downgrade ubuntu to 20.04 (#99) by @JonasBa + +## 0.1.0-alpha.1 + +### Various fixes & improvements + +- fix(linux): if dlopen fails, build from source (63632046) by @JonasBa +- fix(build): avoid from compiling the build script (#98) by @JonasBa +- fix(test): remove only (6cd37259) by @JonasBa +- feat(ci): run jest with --silent (#96) by @JonasBa +- feat(profiling): add profile context (#95) by @JonasBa +- feat(profiling): discard profiles with <= 1 sample (#94) by @JonasBa +- build(binaries): prebuild binaries for more arch (#92) by @JonasBa + +## 0.0.13 + +### Various fixes & improvements + +- fix(segfault): fix return value order (#89) by @JonasBa +- fix(uuid): uuid is 32hex in sentry (#91) by @JonasBa +- test(build): add node 19 to build matrix (#87) by @JonasBa +- build(deps): bump json5 from 2.2.1 to 2.2.3 (#86) by @dependabot +- feat(profiling): add debug logs (#82) by @JonasBa +- fix(sampleRate): profilesSampleRate and tracesSampleRate are multiplied (#77) by @JonasBa +- fix(sdk): use release instead of sdk release (#76) by @JonasBa +- feat(filename): generate filename from abs path (#75) by @JonasBa +- fix: path -> absPath (#72) by @JonasBa + +## 0.0.12 + +### Various fixes & improvements + +- fix(timestamps): int64_t (#69) by @JonasBa + +## 0.0.11 + +### Various fixes & improvements + +- fix(stack): use unique pointer not i (#68) by @JonasBa + +## 0.0.10 + +### Various fixes & improvements + +- fix(profile): wrong stack indexing insertion (#67) by @JonasBa +- docs: readme semicolon (#66) by @scttcper +- Update README.md (595ebb99) by @JonasBa + +## 0.0.9 + +### Various fixes & improvements + +- fix(envelope): missmatch in type guard (#65) by @JonasBa +- feat(binaries): precompile binaries (#64) by @JonasBa + +## 0.0.8 + +### Various fixes & improvements + +- fix(profiling): remove build script (c5cf7353) by @JonasBa + +## 0.0.7 + +### Various fixes & improvements + +- ref(sdk): remove spans (#62) by @JonasBa +- feat(profiling): use env variable instead of compile time (#61) by @JonasBa + +## 0.0.6 + +### Various fixes & improvements + +- ref(sdk): add temporary spans (#60) by @JonasBa +- ref(sdk): remove sdk tag (#59) by @JonasBa +- fix: fix javascript repo reference (af046f28) by @JonasBa + +## 0.0.5 + +### Various fixes & improvements + +- feat(profiling): index stacks (#58) by @JonasBa +- test(config): skip benchmarks (ee8e4d90) by @JonasBa +- chore(pkg): add github (#57) by @JonasBa +- test(samples): bump min samples (123928a2) by @JonasBa + +## 0.0.4 + +### Various fixes & improvements + +- chore(license): switch to MIT (#56) by @JonasBa +- fix(release): js not sh (27482070) by @JonasBa + +## 0.0.3 + +### Various fixes & improvements + +- fix(test): fix setTag test (096619d9) by @JonasBa +- chore(deps): bump (331e8450) by @JonasBa +- fix(profiler): fix typo (f1cb8823) by @JonasBa +- feat(profiler): set logging mode as tag (f2517f69) by @JonasBa + +## 0.0.2 + +### Various fixes & improvements + +- ref(benchmark): add jest benchmark (#54) by @JonasBa +- build(gyp): add compile time flag for profiler logging strategy (#55) by @JonasBa + +## 0.0.1 + +### Various fixes & improvements + +- feat(profile): log call site info (#53) by @JonasBa +- fix(benchmark): recompute json (eddcad28) by @JonasBa +- fix(benchmark): pass both options (b9911726) by @JonasBa +- fix(benchmark): pass option to compare (ce805797) by @JonasBa +- fix(benchmark): run node benchmark instead of compare (0a4d9abb) by @JonasBa +- fix(benchmark): run node benchmark (595a4f96) by @JonasBa +- fix(scripts): stash and apply script results (3d4c92df) by @JonasBa +- fix(scripts): stash and apply script results (a9e7f360) by @JonasBa +- fix(benchmark): allow running between two commits (#52) by @JonasBa +- test(threshold) increase max sample threshold (847f42ac) by @JonasBa +- test(threshold) increase max sample threshold (6d10b885) by @JonasBa +- feat(profiling): adjust sampling frequency (#50) by @JonasBa +- chore(github): add issue bug template (d5b488ad) by @JonasBa +- chore(github): add feature template (f137585b) by @JonasBa +- chore(github): add pull request template (759237f0) by @JonasBa + +## 0.0.0-alpha.6 + +### Various fixes & improvements + +- feat(sdk): use uuid to avoid ignored transactions (#47) by @JonasBa +- fix(profiling): correct ts (b46547f6) by @JonasBa +- feat(sdk): add max duration timeout (#46) by @JonasBa +- fix(units): report ns to backend (#45) by @JonasBa +- feat(timestamps): more accurate timestamps (#44) by @JonasBa +- ref(hubextension): explain finish reference (05924fe7) by @JonasBa +- fix(sampling): remove unnecessary negate (1b6fdab5) by @JonasBa +- fix(profile): rename fields and eval device info only once (#43) by @JonasBa +- feat(skd): add better messaging when we cannot patch the lib (#42) by @JonasBa +- chore(github): add contributing (#41) by @JonasBa + +## 0.0.0-alpha.5 + +### Various fixes & improvements + +- fix(c++): make sure we use unique_id (#40) by @JonasBa +- chore(readme): improve wording (5f439bba) by @JonasBa +- chore(workers): remove disclaimer as we do not support node 10 (ac15d4f7) by @JonasBa + +## 0.0.0-alpha.4 + +### Various fixes & improvements + +- chore(readme): overhead explanation (c159e6bd) by @JonasBa +- chore(readme): overhead explanation (58d865c5) by @JonasBa +- feat(playground): add express node test (#39) by @JonasBa +- feat(c++): cleanup addon data (#38) by @JonasBa +- feat(playground): add express integration (#37) by @JonasBa +- chore(readme): remove todo (505888e7) by @JonasBa + +## 0.0.0-alpha.3 + +### Various fixes & improvements + +- deps(nan): move to dependencies (#36) by @JonasBa + +## 0.0.0-alpha.2 + +### Various fixes & improvements + +- feat(build): move node-gyp to dep (#33) by @JonasBa + +## 0.0.0-alpha.1 + +### Various fixes & improvements + +- ci: use npm pack (#32) by @JonasBa +- fix(ci): remove dependant job (#31) by @JonasBa +- ci(craft): setup build (#30) by @JonasBa +- fix(ci): run ci for release (#28) by @JonasBa +- chore(craft): add bump version script (#26) by @JonasBa +- chore(changelog): add changelog (#25) by @JonasBa diff --git a/packages/profiling-node/LICENSE b/packages/profiling-node/LICENSE new file mode 100644 index 000000000000..6031123bdc4c --- /dev/null +++ b/packages/profiling-node/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Sentry + +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/profiling-node/README.md b/packages/profiling-node/README.md new file mode 100644 index 000000000000..82d7ea97b4c6 --- /dev/null +++ b/packages/profiling-node/README.md @@ -0,0 +1,301 @@ +

+ + Sentry + +

+ +# Official Sentry Profiling SDK for NodeJS + +[![npm version](https://img.shields.io/npm/v/@sentry/profiling-node.svg)](https://www.npmjs.com/package/@sentry/profiling-node) +[![npm dm](https://img.shields.io/npm/dm/@sentry/profiling-node.svg)](https://www.npmjs.com/package/@sentry/profiling-node) +[![npm dt](https://img.shields.io/npm/dt/@sentry/profiling-node.svg)](https://www.npmjs.com/package/@sentry/profiling-node) + +## Installation + +Profiling works as an extension of tracing so you will need both @sentry/node and @sentry/profiling-node installed. The +minimum required major version of @sentry/node that supports profiling is 7.x. + +```bash +# Using yarn +yarn add @sentry/node @sentry/profiling-node + +# Using npm +npm install --save @sentry/node @sentry/profiling-node +``` + +## Usage + +```javascript +import * as Sentry from '@sentry/node'; +import { ProfilingIntegration } from '@sentry/profiling-node'; + +Sentry.init({ + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + debug: true, + tracesSampleRate: 1, + profilesSampleRate: 1, // Set profiling sampling rate. + integrations: [new ProfilingIntegration()], +}); +``` + +Sentry SDK will now automatically profile all transactions, even the ones which may be started as a result of using an +automatic instrumentation integration. + +```javascript +const transaction = Sentry.startTransaction({ name: 'some workflow' }); + +// The code between startTransaction and transaction.finish will be profiled + +transaction.finish(); +``` + +### Building the package from source + +Profiling uses native modules to interop with the v8 javascript engine which means that you may be required to build it +from source. The libraries required to successfully build the package from source are often the same libraries that are +already required to build any other package which uses native modules and if your codebase uses any of those modules, +there is a fairly good chance this will work out of the box. The required packages are python, make and g++. + +**Windows:** If you are building on windows, you may need to install windows-build-tools + +```bash + +# using yarn package manager +yarn global add windows-build-tools +# or npm package manager +npm i -g windows-build-tools +``` + +### Prebuilt binaries + +We currently ship prebuilt binaries for a few of the most common platforms and node versions (v16-20). + +- macOS x64 +- Linux ARM64 (musl) +- Linux x64 (glibc) +- Windows x64 + +For a more detailed list, see job_compile_bindings_profiling_node job in our build.yml github action workflow. + +### Bundling + +If you are looking to squeeze some extra performance or improve cold start in your application (especially true for +serverless environments where modules are often evaluates on a per request basis), then we recommend you look into +bundling your code. Modern JS engines are much faster at parsing and compiling JS than following long module resolution +chains and reading file contents from disk. Because @sentry/profiling-node is a package that uses native node modules, +bundling it is slightly different than just bundling javascript. In other words, the bundler needs to recognize that a +.node file is node native binding and move it to the correct location so that it can later be used. Failing to do so +will result in a MODULE_NOT_FOUND error. + +The easiest way to make bundling work with @sentry/profiling-node and other modules which use native nodejs bindings is +to mark the package as external - this will prevent the code from the package from being bundled, but it means that you +will now need to rely on the package to be installed in your production environment. + +To mark the package as external, use the following configuration: + +[Next.js 13+](https://nextjs.org/docs/app/api-reference/next-config-js/serverComponentsExternalPackages) + +```js +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + // Add the "@sentry/profiling-node" to serverComponentsExternalPackages. + serverComponentsExternalPackages: ['@sentry/profiling-node'], + }, +}; + +module.exports = withSentryConfig(nextConfig, { + /* ... */ +}); +``` + +[webpack](https://webpack.js.org/configuration/externals/#externals) + +```js +externals: { + "@sentry/profiling-node": "commonjs @sentry/profiling-node", +}, +``` + +[esbuild](https://esbuild.github.io/api/#external) + +```js +{ + entryPoints: ['index.js'], + platform: 'node', + external: ['@sentry/profiling-node'], +} +``` + +[Rollup](https://rollupjs.org/configuration-options/#external) + +```js +{ + entry: 'index.js', + external: '@sentry/profiling-node' +} +``` + +[serverless-esbuild (serverless.yml)](https://www.serverless.com/plugins/serverless-esbuild#external-dependencies) + +```yml +custom: + esbuild: + external: + - @sentry/profiling-node + packagerOptions: + scripts: + - npm install @sentry/profiling-node +``` + +[vercel-ncc](https://github.com/vercel/ncc#programmatically-from-nodejs) + +```js +{ + externals: ["@sentry/profiling-node"], +} +``` + +[vite](https://vitejs.dev/config/ssr-options.html#ssr-external) + +```js +ssr: { + external: ['@sentry/profiling-node']; +} +``` + +Marking the package as external is the simplest and most future proof way of ensuring it will work, however if you want +to bundle it, it is possible to do so as well. Bundling has the benefit of improving your script startup time as all of +the code is (usually) inside a single executable .js file, which saves time on module resolution. + +In general, when attempting to bundle .node native file extensions, you will need to tell your bundler how to treat +these, as by default it does not know how to handle them. The required approach varies between build tools and you will +need to find which one will work for you. + +The result of bundling .node files correctly is that they are placed into your bundle output directory with their +require paths updated to reflect their final location. + +Example of bundling @sentry/profiling-node with esbuild and .copy loader + +```json +// package.json +{ + "scripts": "node esbuild.serverless.js" +} +``` + +```js +// esbuild.serverless.js +const { sentryEsbuildPlugin } = require('@sentry/esbuild-plugin'); + +require('esbuild').build({ + entryPoints: ['./index.js'], + outfile: './dist', + platform: 'node', + bundle: true, + minify: true, + sourcemap: true, + // This is no longer necessary + // external: ["@sentry/profiling-node"], + loader: { + // ensures .node binaries are copied to ./dist + '.node': 'copy', + }, + plugins: [ + // See https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/esbuild/ + sentryEsbuildPlugin({ + project: '', + org: '', + authToken: '', + release: '', + sourcemaps: { + // Specify the directory containing build artifacts + assets: './dist/**', + }, + }), + ], +}); +``` + +Once you run `node esbuild.serverless.js` esbuild wil bundle and output the files to ./dist folder, but note that all of +the binaries will be copied. This is wasteful as you will likely only need one of these libraries to be available during +runtime. + +To prune the other libraries, profiling-node ships with a small utility script that helps you prune unused binaries. The +script can be invoked via `sentry-prune-profiler-binaries`, use `--help` to see a list of available options or +`--dry-run` if you want it to log the binaries that would have been deleted. + +Example of only preserving a binary to run node16 on linux x64 musl. + +```bash +sentry-prune-profiler-binaries --target_dir_path=./dist --target_platform=linux --target_node=16 --target_stdlib=musl --target_arch=x64 +``` + +Which will output something like + +``` +Sentry: pruned ./dist/sentry_cpu_profiler-darwin-x64-108-IFGH3SUR.node (90.41 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-darwin-x64-93-Q7KBVHSP.node (74.16 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-arm64-glibc-108-NXSISRTB.node (52.17 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-arm64-glibc-83-OEQT5HUK.node (52.08 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-arm64-glibc-93-IIXXW2PN.node (52.06 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-arm64-musl-108-DSILNYHA.node (48.46 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-arm64-musl-83-4CNOBNC3.node (48.37 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-arm64-musl-93-JA5PKNWQ.node (48.38 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-x64-glibc-108-NXSISRTB.node (52.17 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-x64-glibc-83-OEQT5HUK.node (52.08 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-x64-glibc-93-IIXXW2PN.node (52.06 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-x64-musl-108-CX7SL27U.node (51.50 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-linux-x64-musl-83-YD7ZQK2E.node (51.53 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-win32-x64-108-P7V3URQV.node (181.50 KiB) +Sentry: pruned ./dist/sentry_cpu_profiler-win32-x64-93-3PKQDSGE.node (181.50 KiB) +✅ Sentry: pruned 15 binaries, saved 1.06 MiB in total. +``` + +### Environment flags + +The default mode of the v8 CpuProfiler is kEagerLoggin which enables the profiler even when no profiles are active - +this is good because it makes calls to startProfiling fast at the tradeoff for constant CPU overhead. The behavior can +be controlled via the `SENTRY_PROFILER_LOGGING_MODE` environment variable with values of `eager|lazy`. If you opt to use +the lazy logging mode, calls to startProfiling may be slow (depending on environment and node version, it can be in the +order of a few hundred ms). + +Example of starting a server with lazy logging mode. + +```javascript +SENTRY_PROFILER_LOGGING_MODE=lazy node server.js +``` + +## FAQ 💭 + +### Can the profiler leak PII to Sentry? + +The profiler does not collect function arguments so leaking any PII is unlikely. We only collect a subset of the values +which may identify the device and os that the profiler is running on (if you are already using tracing, it is likely +that these values are already being collected by the SDK). + +There is one way a profiler could leak pii information, but this is unlikely and would only happen for cases where you +might be creating or naming functions which might contain pii information such as + +```js +eval('function scriptFor${PII_DATA}....'); +``` + +In that case it is possible that the function name may end up being reported to Sentry. + +### Are worker threads supported? + +No. All instances of the profiler are scoped per thread In practice, this means that starting a transaction on thread A +and delegating work to thread B will only result in sample stacks being collected from thread A. That said, nothing +should prevent you from starting a transaction on thread B concurrently which will result in two independant profiles +being sent to the Sentry backend. We currently do not do any correlation between such transactions, but we would be open +to exploring the possibilities. Please file an issue if you have suggestions or specific use-cases in mind. + +### How much overhead will this profiler add? + +The profiler uses the kEagerLogging option by default which trades off fast calls to startProfiling for a small amount +of constant CPU overhead. If you are using kEagerLogging then the tradeoff is reversed and there will be a small CPU +overhead while the profiler is not running, but calls to startProfiling could be slow (in our tests, this varies by +environments and node versions, but could be in the order of a couple 100ms). diff --git a/packages/profiling-node/binding.gyp b/packages/profiling-node/binding.gyp new file mode 100644 index 000000000000..fd2322db4e94 --- /dev/null +++ b/packages/profiling-node/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "sentry_cpu_profiler", + "sources": [ "bindings/cpu_profiler.cc" ], + # Silence gcc8 deprecation warning https://github.com/nodejs/nan/issues/807#issuecomment-455750192 + "cflags": ["-Wno-cast-function-type"] + }, + ] +} diff --git a/packages/profiling-node/bindings/cpu_profiler.cc b/packages/profiling-node/bindings/cpu_profiler.cc new file mode 100644 index 000000000000..f269990f425b --- /dev/null +++ b/packages/profiling-node/bindings/cpu_profiler.cc @@ -0,0 +1,1118 @@ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const uint8_t kMaxStackDepth(128); +static const float kSamplingFrequency(99.0); // 99 to avoid lockstep sampling +static const float kSamplingHz(1 / kSamplingFrequency); +static const int kSamplingInterval(kSamplingHz * 1e6); +static const v8::CpuProfilingNamingMode + kNamingMode(v8::CpuProfilingNamingMode::kDebugNaming); +static const v8::CpuProfilingLoggingMode + kDefaultLoggingMode(v8::CpuProfilingLoggingMode::kEagerLogging); + +// Allow users to override the default logging mode via env variable. This is +// useful because sometimes the flow of the profiled program can be to execute +// many sequential transaction - in that case, it may be preferable to set eager +// logging to avoid paying the high cost of profiling for each individual +// transaction (one example for this are jest tests when run with --runInBand +// option). +static const char *kEagerLoggingMode = "eager"; +static const char *kLazyLoggingMode = "lazy"; + +v8::CpuProfilingLoggingMode GetLoggingMode() { + static const char *logging_mode(getenv("SENTRY_PROFILER_LOGGING_MODE")); + + // most times this wont be set so just bail early + if (!logging_mode) { + return kDefaultLoggingMode; + } + + // other times it'll likely be set to lazy as eager is the default + if (strcmp(logging_mode, kLazyLoggingMode) == 0) { + return v8::CpuProfilingLoggingMode::kLazyLogging; + } else if (strcmp(logging_mode, kEagerLoggingMode) == 0) { + return v8::CpuProfilingLoggingMode::kEagerLogging; + } + + return kDefaultLoggingMode; +} + +class SentryProfile; +class Profiler; + +enum class ProfileStatus { + kNotStarted, + kStarted, + kStopped, +}; + +class MeasurementsTicker { +private: + uv_timer_t timer; + uint64_t period_ms; + std::unordered_map> + heap_listeners; + std::unordered_map> + cpu_listeners; + v8::Isolate *isolate; + v8::HeapStatistics heap_stats; + uv_cpu_info_t cpu_stats; + +public: + MeasurementsTicker(uv_loop_t *loop) + : period_ms(100), isolate(v8::Isolate::GetCurrent()) { + uv_timer_init(loop, &timer); + timer.data = this; + } + + static void ticker(uv_timer_t *); + // Memory listeners + void heap_callback(); + void add_heap_listener( + std::string &profile_id, + const std::function cb); + void remove_heap_listener( + std::string &profile_id, + const std::function &cb); + + // CPU listeners + void cpu_callback(); + void add_cpu_listener(std::string &profile_id, + const std::function cb); + void remove_cpu_listener(std::string &profile_id, + const std::function &cb); + + size_t listener_count(); + + ~MeasurementsTicker() { + uv_timer_stop(&timer); + + auto handle = reinterpret_cast(&timer); + + // Calling uv_close on an inactive handle will cause a segfault. + if (uv_is_active(handle)) { + uv_close(handle, nullptr); + } + } +}; + +size_t MeasurementsTicker::listener_count() { + return heap_listeners.size() + cpu_listeners.size(); +} + +// Heap tickers +void MeasurementsTicker::heap_callback() { + isolate->GetHeapStatistics(&heap_stats); + uint64_t ts = uv_hrtime(); + + for (auto cb : heap_listeners) { + cb.second(ts, heap_stats); + } +} + +void MeasurementsTicker::add_heap_listener( + std::string &profile_id, + const std::function cb) { + heap_listeners.emplace(profile_id, cb); + + if (listener_count() == 1) { + uv_timer_set_repeat(&timer, period_ms); + uv_timer_start(&timer, ticker, 0, period_ms); + } +} + +void MeasurementsTicker::remove_heap_listener( + std::string &profile_id, + const std::function &cb) { + heap_listeners.erase(profile_id); + + if (listener_count() == 0) { + uv_timer_stop(&timer); + } +}; + +// CPU tickers +void MeasurementsTicker::cpu_callback() { + uv_cpu_info_t *cpu = &cpu_stats; + int count; + int err = uv_cpu_info(&cpu, &count); + + if (err) { + return; + } + + if (count < 1) { + return; + } + + uint64_t ts = uv_hrtime(); + uint64_t total = 0; + uint64_t idle_total = 0; + + for (int i = 0; i < count; i++) { + uv_cpu_info_t *core = cpu + i; + + total += core->cpu_times.user; + total += core->cpu_times.nice; + total += core->cpu_times.sys; + total += core->cpu_times.idle; + total += core->cpu_times.irq; + + idle_total += core->cpu_times.idle; + } + + double idle_avg = idle_total / count; + double total_avg = total / count; + double rate = 1.0 - idle_avg / total_avg; + + if (rate < 0.0 || isinf(rate) || isnan(rate)) { + rate = 0.0; + } + + auto it = cpu_listeners.begin(); + while (it != cpu_listeners.end()) { + if (it->second(ts, rate)) { + it = cpu_listeners.erase(it); + } else { + ++it; + } + }; + + uv_free_cpu_info(cpu, count); +}; + +void MeasurementsTicker::ticker(uv_timer_t *handle) { + MeasurementsTicker *self = static_cast(handle->data); + self->heap_callback(); + self->cpu_callback(); +} + +void MeasurementsTicker::add_cpu_listener( + std::string &profile_id, const std::function cb) { + cpu_listeners.emplace(profile_id, cb); + + if (listener_count() == 1) { + uv_timer_set_repeat(&timer, period_ms); + uv_timer_start(&timer, ticker, 0, period_ms); + } +} + +void MeasurementsTicker::remove_cpu_listener( + std::string &profile_id, const std::function &cb) { + cpu_listeners.erase(profile_id); + + if (listener_count() == 0) { + uv_timer_stop(&timer); + } +}; + +class Profiler { +public: + std::unordered_map active_profiles; + + MeasurementsTicker measurements_ticker; + v8::CpuProfiler *cpu_profiler; + + explicit Profiler(const napi_env &env, v8::Isolate *isolate) + : measurements_ticker(uv_default_loop()), + cpu_profiler( + v8::CpuProfiler::New(isolate, kNamingMode, GetLoggingMode())) {} +}; + +class SentryProfile { +private: + uint64_t started_at; + uint16_t heap_write_index = 0; + uint16_t cpu_write_index = 0; + + std::vector heap_stats_ts; + std::vector heap_stats_usage; + + std::vector cpu_stats_ts; + std::vector cpu_stats_usage; + + const std::function memory_sampler_cb; + const std::function cpu_sampler_cb; + + ProfileStatus status = ProfileStatus::kNotStarted; + std::string id; + +public: + explicit SentryProfile(const char *id) + : started_at(uv_hrtime()), + memory_sampler_cb([this](uint64_t ts, v8::HeapStatistics &stats) { + if ((heap_write_index >= heap_stats_ts.capacity()) || + heap_write_index >= heap_stats_usage.capacity()) { + return true; + } + + heap_stats_ts.insert(heap_stats_ts.begin() + heap_write_index, + ts - started_at); + heap_stats_usage.insert( + heap_stats_usage.begin() + heap_write_index, + static_cast(stats.used_heap_size())); + ++heap_write_index; + + return false; + }), + + cpu_sampler_cb([this](uint64_t ts, double rate) { + if (cpu_write_index >= cpu_stats_ts.capacity() || + cpu_write_index >= cpu_stats_usage.capacity()) { + return true; + } + cpu_stats_ts.insert(cpu_stats_ts.begin() + cpu_write_index, + ts - started_at); + cpu_stats_usage.insert(cpu_stats_usage.begin() + cpu_write_index, + rate); + ++cpu_write_index; + return false; + }), + + status(ProfileStatus::kNotStarted), id(id) { + heap_stats_ts.reserve(300); + heap_stats_usage.reserve(300); + cpu_stats_ts.reserve(300); + cpu_stats_usage.reserve(300); + } + + const std::vector &heap_usage_timestamps() const; + const std::vector &heap_usage_values() const; + const uint16_t &heap_usage_write_index() const; + + const std::vector &cpu_usage_timestamps() const; + const std::vector &cpu_usage_values() const; + const uint16_t &cpu_usage_write_index() const; + + void Start(Profiler *profiler); + v8::CpuProfile *Stop(Profiler *profiler); +}; + +void SentryProfile::Start(Profiler *profiler) { + v8::Local profile_title = + v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), id.c_str(), + v8::NewStringType::kNormal) + .ToLocalChecked(); + + started_at = uv_hrtime(); + + // Initialize the CPU Profiler + profiler->cpu_profiler->StartProfiling( + profile_title, + {v8::CpuProfilingMode::kCallerLineNumbers, + v8::CpuProfilingOptions::kNoSampleLimit, kSamplingInterval}); + + // listen for memory sample ticks + profiler->measurements_ticker.add_cpu_listener(id, cpu_sampler_cb); + profiler->measurements_ticker.add_heap_listener(id, memory_sampler_cb); + + status = ProfileStatus::kStarted; +} + +static void CleanupSentryProfile(Profiler *profiler, + SentryProfile *sentry_profile, + const std::string &profile_id) { + if (sentry_profile == nullptr) { + return; + } + + sentry_profile->Stop(profiler); + profiler->active_profiles.erase(profile_id); + delete sentry_profile; +}; + +v8::CpuProfile *SentryProfile::Stop(Profiler *profiler) { + // Stop the CPU Profiler + v8::CpuProfile *profile = profiler->cpu_profiler->StopProfiling( + v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), id.c_str(), + v8::NewStringType::kNormal) + .ToLocalChecked()); + + // Remove the meemory sampler + profiler->measurements_ticker.remove_heap_listener(id, memory_sampler_cb); + profiler->measurements_ticker.remove_cpu_listener(id, cpu_sampler_cb); + // If for some reason stopProfiling was called with an invalid profile title + // or if that title had somehow been stopped already, profile will be null. + status = ProfileStatus::kStopped; + return profile; +} + +// Memory getters +const std::vector &SentryProfile::heap_usage_timestamps() const { + return heap_stats_ts; +}; + +const std::vector &SentryProfile::heap_usage_values() const { + return heap_stats_usage; +}; + +const uint16_t &SentryProfile::heap_usage_write_index() const { + return heap_write_index; +}; + +// CPU getters +const std::vector &SentryProfile::cpu_usage_timestamps() const { + return cpu_stats_ts; +}; + +const std::vector &SentryProfile::cpu_usage_values() const { + return cpu_stats_usage; +}; +const uint16_t &SentryProfile::cpu_usage_write_index() const { + return cpu_write_index; +}; + +#ifdef _WIN32 +static const char kPlatformSeparator = '\\'; +static const char kWinDiskPrefix = ':'; +#else +static const char kPlatformSeparator = '/'; +#endif + +static const char kSentryPathDelimiter = '.'; +static const char kSentryFileDelimiter = ':'; +static const std::string kNodeModulesPath = + std::string("node_modules") + kPlatformSeparator; + +static void GetFrameModule(const std::string &abs_path, std::string &module) { + if (abs_path.empty()) { + return; + } + + module = abs_path; + + // Drop .js extension + size_t module_len = module.length(); + if (module.compare(module_len - 3, 3, ".js") == 0) { + module = module.substr(0, module_len - 3); + } + + // Drop anything before and including node_modules/ + size_t node_modules_pos = module.rfind(kNodeModulesPath); + if (node_modules_pos != std::string::npos) { + module = module.substr(node_modules_pos + 13); + } + + // Replace all path separators with dots except the last one, that one is + // replaced with a colon + int match_count = 0; + for (int pos = module.length() - 1; pos >= 0; pos--) { + // if there is a match and it's not the first character, replace it + if (module[pos] == kPlatformSeparator) { + module[pos] = + match_count == 0 ? kSentryFileDelimiter : kSentryPathDelimiter; + match_count++; + } + } + +#ifdef _WIN32 + // Strip out C: prefix. On Windows, the drive letter is not part of the module + // name + if (module[1] == kWinDiskPrefix) { + // We will try and strip our the disk prefix. + module = module.substr(2, std::string::npos); + } +#endif + + if (module[0] == '.') { + module = module.substr(1, std::string::npos); + } +} + +static napi_value GetFrameModuleWrapped(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); + + size_t len; + assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok); + + char *abs_path = (char *)malloc(len + 1); + assert(napi_get_value_string_utf8(env, argv[0], abs_path, len + 1, &len) == + napi_ok); + + std::string module; + napi_value napi_module; + + GetFrameModule(abs_path, module); + + assert(napi_create_string_utf8(env, module.c_str(), NAPI_AUTO_LENGTH, + &napi_module) == napi_ok); + return napi_module; +} + +napi_value +CreateFrameNode(const napi_env &env, const v8::CpuProfileNode &node, + std::unordered_map &module_cache, + napi_value &resources) { + napi_value js_node; + napi_create_object(env, &js_node); + + napi_value lineno_prop; + napi_create_int32(env, node.GetLineNumber(), &lineno_prop); + napi_set_named_property(env, js_node, "lineno", lineno_prop); + + napi_value colno_prop; + napi_create_int32(env, node.GetColumnNumber(), &colno_prop); + napi_set_named_property(env, js_node, "colno", colno_prop); + + if (node.GetSourceType() != v8::CpuProfileNode::SourceType::kScript) { + napi_value system_frame_prop; + napi_get_boolean(env, false, &system_frame_prop); + napi_set_named_property(env, js_node, "in_app", system_frame_prop); + } + + napi_value function; + napi_create_string_utf8(env, node.GetFunctionNameStr(), NAPI_AUTO_LENGTH, + &function); + napi_set_named_property(env, js_node, "function", function); + + const char *resource = node.GetScriptResourceNameStr(); + + if (resource != nullptr) { + // resource is absolute path, set it on the abs_path property + napi_value abs_path_prop; + napi_create_string_utf8(env, resource, NAPI_AUTO_LENGTH, &abs_path_prop); + napi_set_named_property(env, js_node, "abs_path", abs_path_prop); + // Error stack traces are not relative to root dir, doing our own path + // normalization breaks people's code mapping configs so we need to leave it + // as is. + napi_set_named_property(env, js_node, "filename", abs_path_prop); + + std::string module; + std::string resource_str = std::string(resource); + + if (resource_str.empty()) { + return js_node; + } + + if (module_cache.find(resource_str) != module_cache.end()) { + module = module_cache[resource_str]; + } else { + napi_value resource; + napi_create_string_utf8(env, resource_str.c_str(), NAPI_AUTO_LENGTH, + &resource); + napi_set_element(env, resources, module_cache.size(), resource); + + GetFrameModule(resource_str, module); + module_cache.emplace(resource_str, module); + } + + if (!module.empty()) { + napi_value filename_prop; + napi_create_string_utf8(env, module.c_str(), NAPI_AUTO_LENGTH, + &filename_prop); + napi_set_named_property(env, js_node, "module", filename_prop); + } + } + + return js_node; +}; + +napi_value CreateSample(const napi_env &env, const uint32_t stack_id, + const int64_t sample_timestamp_us, + const uint32_t thread_id) { + napi_value js_node; + napi_create_object(env, &js_node); + + napi_value stack_id_prop; + napi_create_uint32(env, stack_id, &stack_id_prop); + napi_set_named_property(env, js_node, "stack_id", stack_id_prop); + + napi_value thread_id_prop; + napi_create_string_utf8(env, std::to_string(thread_id).c_str(), + NAPI_AUTO_LENGTH, &thread_id_prop); + napi_set_named_property(env, js_node, "thread_id", thread_id_prop); + + napi_value elapsed_since_start_ns_prop; + napi_create_int64(env, sample_timestamp_us * 1000, + &elapsed_since_start_ns_prop); + napi_set_named_property(env, js_node, "elapsed_since_start_ns", + elapsed_since_start_ns_prop); + + return js_node; +}; + +std::string kDelimiter = std::string(";"); +std::string hashCpuProfilerNodeByPath(const v8::CpuProfileNode *node, + std::string &path) { + path.clear(); + + while (node != nullptr) { + path.append(std::to_string(node->GetNodeId())); + node = node->GetParent(); + } + + return path; +} + +static void GetSamples(const napi_env &env, const v8::CpuProfile *profile, + const uint32_t thread_id, napi_value &samples, + napi_value &stacks, napi_value &frames, + napi_value &resources) { + const int64_t profile_start_time_us = profile->GetStartTime(); + const int sampleCount = profile->GetSamplesCount(); + + uint32_t unique_stack_id = 0; + uint32_t unique_frame_id = 0; + + // Initialize the lookup tables for stacks and frames, both of these are + // indexed in the sample format we are using to optimize for size. + std::unordered_map frame_lookup_table; + std::unordered_map stack_lookup_table; + std::unordered_map module_cache; + + // At worst, all stacks are unique so reserve the maximum amount of space + stack_lookup_table.reserve(sampleCount); + + std::string node_hash = ""; + + for (int i = 0; i < sampleCount; i++) { + uint32_t stack_index = unique_stack_id; + + const v8::CpuProfileNode *node = profile->GetSample(i); + const int64_t sample_timestamp = profile->GetSampleTimestamp(i); + + // If a node was only on top of the stack once, then it will only ever + // be inserted once and there is no need for hashing. + if (node->GetHitCount() > 1) { + hashCpuProfilerNodeByPath(node, node_hash); + + std::unordered_map::iterator + stack_index_cache_hit = stack_lookup_table.find(node_hash); + + // If we have a hit, update the stack index, otherwise + // insert it into the hash table and continue. + if (stack_index_cache_hit == stack_lookup_table.end()) { + stack_lookup_table.emplace(node_hash, stack_index); + } else { + stack_index = stack_index_cache_hit->second; + } + } + + napi_value sample = CreateSample( + env, stack_index, sample_timestamp - profile_start_time_us, thread_id); + + if (stack_index != unique_stack_id) { + napi_value index; + napi_create_uint32(env, i, &index); + napi_set_property(env, samples, index, sample); + continue; + } + + // A stack is a list of frames ordered from outermost (top) to innermost + // frame (bottom) + napi_value stack; + napi_create_array(env, &stack); + + uint32_t stack_depth = 0; + + while (node != nullptr && stack_depth < kMaxStackDepth) { + auto nodeId = node->GetNodeId(); + auto frame_index = frame_lookup_table.find(nodeId); + + // If the frame does not exist in the index + if (frame_index == frame_lookup_table.end()) { + frame_lookup_table.emplace(nodeId, unique_frame_id); + + napi_value frame_id; + napi_create_uint32(env, unique_frame_id, &frame_id); + + napi_value depth; + napi_create_uint32(env, stack_depth, &depth); + napi_set_property(env, stack, depth, frame_id); + napi_set_property(env, frames, frame_id, + CreateFrameNode(env, *node, module_cache, resources)); + + unique_frame_id++; + } else { + // If it was already indexed, just add it's id to the stack + napi_value depth; + napi_create_uint32(env, stack_depth, &depth); + + napi_value frame; + napi_create_uint32(env, frame_index->second, &frame); + napi_set_property(env, stack, depth, frame); + }; + + // Continue walking down the stack + node = node->GetParent(); + stack_depth++; + } + + napi_value napi_sample_index; + napi_value napi_stack_index; + + napi_create_uint32(env, i, &napi_sample_index); + napi_set_property(env, samples, napi_sample_index, sample); + napi_create_uint32(env, stack_index, &napi_stack_index); + napi_set_property(env, stacks, napi_stack_index, stack); + + unique_stack_id++; + } +} + +static napi_value +TranslateMeasurementsDouble(const napi_env &env, const char *unit, + const uint16_t size, + const std::vector &values, + const std::vector ×tamps) { + if (size > values.size() || size > timestamps.size()) { + napi_throw_range_error(env, "NAPI_ERROR", + "CPU measurement size is larger than the number of " + "values or timestamps"); + return nullptr; + } + + if (values.size() != timestamps.size()) { + napi_throw_range_error(env, "NAPI_ERROR", + "CPU measurement entries are corrupt, expected " + "values and timestamps to be of equal length"); + return nullptr; + } + + napi_value measurement; + napi_create_object(env, &measurement); + + napi_value unit_string; + napi_create_string_utf8(env, unit, NAPI_AUTO_LENGTH, &unit_string); + napi_set_named_property(env, measurement, "unit", unit_string); + + napi_value values_array; + napi_create_array(env, &values_array); + + uint16_t idx = size; + + for (size_t i = 0; i < idx; i++) { + napi_value entry; + napi_create_object(env, &entry); + + napi_value value; + if (napi_create_double(env, values[i], &value) != napi_ok) { + if (napi_create_double(env, 0.0, &value) != napi_ok) { + continue; + } + } + + napi_value ts; + napi_create_int64(env, timestamps[i], &ts); + + napi_set_named_property(env, entry, "value", value); + napi_set_named_property(env, entry, "elapsed_since_start_ns", ts); + napi_set_element(env, values_array, i, entry); + } + + napi_set_named_property(env, measurement, "values", values_array); + + return measurement; +} + +static napi_value +TranslateMeasurements(const napi_env &env, const char *unit, + const uint16_t size, const std::vector &values, + const std::vector ×tamps) { + if (size > values.size() || size > timestamps.size()) { + napi_throw_range_error(env, "NAPI_ERROR", + "Memory measurement size is larger than the number " + "of values or timestamps"); + return nullptr; + } + + if (values.size() != timestamps.size()) { + napi_throw_range_error(env, "NAPI_ERROR", + "Memory measurement entries are corrupt, expected " + "values and timestamps to be of equal length"); + return nullptr; + } + + napi_value measurement; + napi_create_object(env, &measurement); + + napi_value unit_string; + napi_create_string_utf8(env, unit, NAPI_AUTO_LENGTH, &unit_string); + napi_set_named_property(env, measurement, "unit", unit_string); + + napi_value values_array; + napi_create_array(env, &values_array); + + for (size_t i = 0; i < size; i++) { + napi_value entry; + napi_create_object(env, &entry); + + napi_value value; + napi_create_int64(env, values[i], &value); + + napi_value ts; + napi_create_int64(env, timestamps[i], &ts); + + napi_set_named_property(env, entry, "value", value); + napi_set_named_property(env, entry, "elapsed_since_start_ns", ts); + napi_set_element(env, values_array, i, entry); + } + + napi_set_named_property(env, measurement, "values", values_array); + + return measurement; +} + +static napi_value TranslateProfile(const napi_env &env, + const v8::CpuProfile *profile, + const uint32_t thread_id, + bool collect_resources) { + napi_value js_profile; + + napi_create_object(env, &js_profile); + + napi_value logging_mode; + napi_value samples; + napi_value stacks; + napi_value frames; + napi_value resources; + + napi_create_string_utf8( + env, + GetLoggingMode() == v8::CpuProfilingLoggingMode::kEagerLogging ? "eager" + : "lazy", + NAPI_AUTO_LENGTH, &logging_mode); + + napi_create_array(env, &samples); + napi_create_array(env, &stacks); + napi_create_array(env, &frames); + napi_create_array(env, &resources); + + napi_set_named_property(env, js_profile, "samples", samples); + napi_set_named_property(env, js_profile, "stacks", stacks); + napi_set_named_property(env, js_profile, "frames", frames); + napi_set_named_property(env, js_profile, "profiler_logging_mode", + logging_mode); + + GetSamples(env, profile, thread_id, samples, stacks, frames, resources); + + if (collect_resources) { + napi_set_named_property(env, js_profile, "resources", resources); + } else { + napi_create_array(env, &resources); + napi_set_named_property(env, js_profile, "resources", resources); + } + + return js_profile; +} + +static napi_value StartProfiling(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + + assert(napi_get_cb_info(env, info, &argc, argv, NULL, NULL) == napi_ok); + + napi_valuetype callbacktype0; + assert(napi_typeof(env, argv[0], &callbacktype0) == napi_ok); + + if (callbacktype0 != napi_string) { + napi_throw_error( + env, "NAPI_ERROR", + "TypeError: StartProfiling expects a string as first argument."); + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + size_t len; + assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok); + + char *title = (char *)malloc(len + 1); + assert(napi_get_value_string_utf8(env, argv[0], title, len + 1, &len) == + napi_ok); + + if (len < 1) { + napi_throw_error(env, "NAPI_ERROR", + "StartProfiling expects a non-empty string as first " + "argument, got an empty string."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + assert(isolate != 0); + + Profiler *profiler; + assert(napi_get_instance_data(env, (void **)&profiler) == napi_ok); + + if (!profiler) { + napi_throw_error(env, "NAPI_ERROR", + "StartProfiling: Profiler is not initialized."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + const std::string profile_id(title); + // In case we have a collision, cleanup the old profile first + auto existing_profile = profiler->active_profiles.find(profile_id); + if (existing_profile != profiler->active_profiles.end()) { + existing_profile->second->Stop(profiler); + CleanupSentryProfile(profiler, existing_profile->second, profile_id); + } + + SentryProfile *sentry_profile = new SentryProfile(title); + sentry_profile->Start(profiler); + + profiler->active_profiles.emplace(profile_id, sentry_profile); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; +} + +// StopProfiling(string title) +// https://v8docs.nodesource.com/node-18.2/d2/d34/classv8_1_1_cpu_profiler.html#a40ca4c8a8aa4c9233aa2a2706457cc80 +static napi_value StopProfiling(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value argv[3]; + + assert(napi_get_cb_info(env, info, &argc, argv, NULL, NULL) == napi_ok); + + if (argc < 2) { + napi_throw_error(env, "NAPI_ERROR", + "StopProfiling expects at least two arguments."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + // Verify the first argument is a string + napi_valuetype callbacktype0; + assert(napi_typeof(env, argv[0], &callbacktype0) == napi_ok); + + if (callbacktype0 != napi_string) { + napi_throw_error(env, "NAPI_ERROR", + "StopProfiling expects a string as first argument."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + // Verify the second argument is a number + napi_valuetype callbacktype1; + assert(napi_typeof(env, argv[1], &callbacktype1) == napi_ok); + + if (callbacktype1 != napi_number) { + napi_throw_error( + env, "NAPI_ERROR", + "StopProfiling expects a thread_id integer as second argument."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + size_t len; + assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok); + + char *title = (char *)malloc(len + 1); + assert(napi_get_value_string_utf8(env, argv[0], title, len + 1, &len) == + napi_ok); + + if (len < 1) { + napi_throw_error(env, "NAPI_ERROR", + "StopProfiling expects a string as first argument."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + // Get the value of the second argument and convert it to uint64 + int64_t thread_id; + assert(napi_get_value_int64(env, argv[1], &thread_id) == napi_ok); + + // Get profiler from instance data + Profiler *profiler; + assert(napi_get_instance_data(env, (void **)&profiler) == napi_ok); + + if (!profiler) { + napi_throw_error(env, "NAPI_ERROR", + "StopProfiling: Profiler is not initialized."); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + + return napi_null; + } + + const std::string profile_id(title); + auto profile = profiler->active_profiles.find(profile_id); + + // If the profile was never started, silently ignore the call and return null + if (profile == profiler->active_profiles.end()) { + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + return napi_null; + } + + v8::CpuProfile *cpu_profile = profile->second->Stop(profiler); + + // If for some reason stopProfiling was called with an invalid profile title + // or if that title had somehow been stopped already, profile will be null. + if (!cpu_profile) { + CleanupSentryProfile(profiler, profile->second, profile_id); + + napi_value napi_null; + assert(napi_get_null(env, &napi_null) == napi_ok); + return napi_null; + }; + + napi_valuetype callbacktype3; + assert(napi_typeof(env, argv[2], &callbacktype3) == napi_ok); + + bool collect_resources; + napi_get_value_bool(env, argv[2], &collect_resources); + + napi_value js_profile = + TranslateProfile(env, cpu_profile, thread_id, collect_resources); + + napi_value measurements; + napi_create_object(env, &measurements); + + if (profile->second->heap_usage_write_index() > 0) { + static const char *memory_unit = "byte"; + napi_value heap_usage_measurements = TranslateMeasurements( + env, memory_unit, profile->second->heap_usage_write_index(), + profile->second->heap_usage_values(), + profile->second->heap_usage_timestamps()); + + if (heap_usage_measurements != nullptr) { + napi_set_named_property(env, measurements, "memory_footprint", + heap_usage_measurements); + }; + }; + + if (profile->second->cpu_usage_write_index() > 0) { + static const char *cpu_unit = "percent"; + napi_value cpu_usage_measurements = TranslateMeasurementsDouble( + env, cpu_unit, profile->second->cpu_usage_write_index(), + profile->second->cpu_usage_values(), + profile->second->cpu_usage_timestamps()); + + if (cpu_usage_measurements != nullptr) { + napi_set_named_property(env, measurements, "cpu_usage", + cpu_usage_measurements); + }; + }; + + napi_set_named_property(env, js_profile, "measurements", measurements); + + CleanupSentryProfile(profiler, profile->second, profile_id); + cpu_profile->Delete(); + + return js_profile; +}; + +void FreeAddonData(napi_env env, void *data, void *hint) { + Profiler *profiler = static_cast(data); + + if (profiler == nullptr) { + return; + } + + if (!profiler->active_profiles.empty()) { + for (auto &profile : profiler->active_profiles) { + CleanupSentryProfile(profiler, profile.second, profile.first); + } + } + + if (profiler->cpu_profiler != nullptr) { + profiler->cpu_profiler->Dispose(); + } + + delete profiler; +} + +napi_value Init(napi_env env, napi_value exports) { + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + + if (isolate == nullptr) { + napi_throw_error(env, nullptr, + "Failed to initialize Sentry profiler: isolate is null."); + return NULL; + } + + Profiler *profiler = new Profiler(env, isolate); + + if (napi_set_instance_data(env, profiler, FreeAddonData, NULL) != napi_ok) { + napi_throw_error(env, nullptr, "Failed to set instance data for profiler."); + return NULL; + } + + napi_value start_profiling; + if (napi_create_function(env, "startProfiling", NAPI_AUTO_LENGTH, + StartProfiling, exports, + &start_profiling) != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create startProfiling function."); + return NULL; + } + + if (napi_set_named_property(env, exports, "startProfiling", + start_profiling) != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to set startProfiling property on exports."); + return NULL; + } + + napi_value stop_profiling; + if (napi_create_function(env, "stopProfiling", NAPI_AUTO_LENGTH, + StopProfiling, exports, + &stop_profiling) != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create stopProfiling function."); + return NULL; + } + + if (napi_set_named_property(env, exports, "stopProfiling", stop_profiling) != + napi_ok) { + napi_throw_error(env, nullptr, + "Failed to set stopProfiling property on exports."); + return NULL; + } + + napi_value get_frame_module; + if (napi_create_function(env, "getFrameModule", NAPI_AUTO_LENGTH, + GetFrameModuleWrapped, exports, + &get_frame_module) != napi_ok) { + napi_throw_error(env, nullptr, "Failed to create getFrameModule function."); + return NULL; + } + + if (napi_set_named_property(env, exports, "getFrameModule", + get_frame_module) != napi_ok) { + napi_throw_error(env, nullptr, + "Failed to set getFrameModule property on exports."); + return NULL; + } + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/packages/profiling-node/clang-format.js b/packages/profiling-node/clang-format.js new file mode 100644 index 000000000000..7deb9fb97993 --- /dev/null +++ b/packages/profiling-node/clang-format.js @@ -0,0 +1,20 @@ +const child_process = require('child_process'); + +const args = ['--Werror', '-i', '--style=file', 'bindings/cpu_profiler.cc']; +const cmd = `./node_modules/.bin/clang-format ${args.join(' ')}`; + +child_process.execSync(cmd); + +// eslint-disable-next-line no-console +console.log('clang-format: done, checking tree...'); + +const diff = child_process.execSync('git status --short').toString(); + +if (diff) { + // eslint-disable-next-line no-console + console.error('clang-format: check failed ❌'); + process.exit(1); +} + +// eslint-disable-next-line no-console +console.log('clang-format: check passed ✅'); diff --git a/packages/profiling-node/jest.config.js b/packages/profiling-node/jest.config.js new file mode 100644 index 000000000000..89bda645921b --- /dev/null +++ b/packages/profiling-node/jest.config.js @@ -0,0 +1,6 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + testEnvironment: 'node', +}; diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json new file mode 100644 index 000000000000..c72cc5c8ae6e --- /dev/null +++ b/packages/profiling-node/package.json @@ -0,0 +1,96 @@ +{ + "name": "@sentry/profiling-node", + "version": "1.3.5", + "description": "Official Sentry SDK for Node.js Profiling", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", + "author": "Sentry", + "license": "MIT", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "typesVersions": { + "<4.9": { + "lib/types/index.d.ts": [ + "lib/types-ts3.8/index.d.ts" + ] + } + }, + "bin": { + "sentry-prune-profiler-binaries": "scripts/prune-profiler-binaries.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "lib", + "bindings", + "binding.gyp", + "LICENSE", + "README.md", + "package.json", + "scripts/binaries.js", + "scripts/check-build.js", + "scripts/copy-target.js", + "scripts/prune-profiler-binaries.js" + ], + "scripts": { + "install": "node scripts/check-build.js", + "clean": "rm -rf build && rm -rf lib", + "lint": "yarn lint:eslint && yarn lint:clang", + "lint:eslint": "eslint . --format stylish", + "lint:clang": "node clang-format.js", + "fix": "eslint . --format stylish --fix", + "lint:fix": "yarn fix:eslint && yarn fix:clang", + "lint:fix:clang": "node clang-format.js --fix", + "build": "yarn build:lib && yarn build:bindings:configure && yarn build:bindings", + "build:lib": "yarn build:types && rollup -c rollup.npm.config.mjs", + "build:transpile": "yarn build:bindings:configure && yarn build:bindings && yarn build:lib", + "build:types:downlevel": "yarn downlevel-dts lib/types lib/types-ts3.8 --to ts3.8", + "build:types": "tsc -p tsconfig.types.json && yarn build:types:downlevel", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:bindings:configure": "node-gyp configure", + "build:bindings:configure:arm64": "node-gyp configure --arch=arm64 --target_arch=arm64", + "build:bindings": "node-gyp build && node scripts/copy-target.js", + "build:bindings:arm64": "node-gyp build --arch=arm64 && node scripts/copy-target.js", + "build:dev": "yarn clean && yarn build:bindings:configure && yarn build", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:tarball": "npm pack", + "test:watch": "cross-env SENTRY_PROFILER_BINARY_DIR=build jest --watch", + "test:bundle": "node test-binaries.esbuild.js", + "test": "cross-env SENTRY_PROFILER_BINARY_DIR=lib jest --config jest.config.js" + }, + "dependencies": { + "detect-libc": "^2.0.2", + "node-abi": "^3.52.0" + }, + "devDependencies": { + "@sentry/core": "7.93.0", + "@sentry/node": "7.93.0", + "@sentry/types": "7.93.0", + "@sentry/utils": "7.93.0", + "@types/node": "16.18.70", + "@types/node-abi": "^3.0.0", + "clang-format": "^1.8.0", + "cross-env": "^7.0.3", + "node-gyp": "^9.4.1", + "typescript": "^4.9.5" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false, + "nx": { + "targets": { + "build:transpile": { + "dependsOn": [ + "^build:transpile", + "^build:types" + ] + } + } + } +} diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs new file mode 100644 index 000000000000..51e812488bb1 --- /dev/null +++ b/packages/profiling-node/rollup.npm.config.mjs @@ -0,0 +1,25 @@ +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; +import { makeBaseNPMConfig, makeNPMConfigVariants, plugins } from '@sentry-internal/rollup-utils'; + +const configs = makeNPMConfigVariants(makeBaseNPMConfig()); +const cjsConfig = configs.find(config => config.output.format === 'cjs'); + +if (!cjsConfig) { + throw new Error('CJS config is required for profiling-node.'); +} + +const config = { + ...cjsConfig, + input: 'src/index.ts', + output: { ...cjsConfig.output, file: 'lib/index.js', format: 'cjs', dir: undefined, preserveModules: false }, + plugins: [ + plugins.makeLicensePlugin('Sentry Node Profiling'), + resolve(), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + ], +}; + +export default config; diff --git a/packages/profiling-node/scripts/binaries.js b/packages/profiling-node/scripts/binaries.js new file mode 100644 index 000000000000..2c0c6be2642b --- /dev/null +++ b/packages/profiling-node/scripts/binaries.js @@ -0,0 +1,27 @@ +const os = require('os'); +const path = require('path'); + +const abi = require('node-abi'); +const libc = require('detect-libc'); + +function getModuleName() { + const stdlib = libc.familySync(); + const platform = process.env['BUILD_PLATFORM'] || os.platform(); + const arch = process.env['BUILD_ARCH'] || os.arch(); + + if (platform === 'darwin' && arch === 'arm64') { + const identifier = [platform, 'arm64', abi.getAbi(process.versions.node, 'node')].filter(Boolean).join('-'); + return `sentry_cpu_profiler-${identifier}.node`; + } + + const identifier = [platform, arch, stdlib, abi.getAbi(process.versions.node, 'node')].filter(Boolean).join('-'); + + return `sentry_cpu_profiler-${identifier}.node`; +} + +const source = path.join(__dirname, '..', 'build', 'Release', 'sentry_cpu_profiler.node'); +const target = path.join(__dirname, '..', 'lib', getModuleName()); + +module.exports.source = source; +module.exports.target = target; +module.exports.getModuleName = getModuleName; diff --git a/packages/profiling-node/scripts/check-build.js b/packages/profiling-node/scripts/check-build.js new file mode 100644 index 000000000000..6892d90ba4b3 --- /dev/null +++ b/packages/profiling-node/scripts/check-build.js @@ -0,0 +1,56 @@ +// This is a build scripts, so some logging is desireable as it allows +// us to follow the code path that triggered the error. +/* eslint-disable no-console */ +const fs = require('fs'); +const child_process = require('child_process'); +const binaries = require('./binaries.js'); + +function clean(err) { + return err.toString().trim(); +} + +function recompileFromSource() { + console.log('@sentry/profiling-node: Compiling from source...'); + let spawn = child_process.spawnSync('npm', ['run', 'build:bindings:configure'], { + stdio: ['inherit', 'inherit', 'pipe'], + env: process.env, + shell: true, + }); + + if (spawn.status !== 0) { + console.log('@sentry/profiling-node: Failed to configure gyp'); + console.log('@sentry/profiling-node:', clean(spawn.stderr)); + return; + } + + spawn = child_process.spawnSync('npm', ['run', 'build:bindings'], { + stdio: ['inherit', 'inherit', 'pipe'], + env: process.env, + shell: true, + }); + if (spawn.status !== 0) { + console.log('@sentry/profiling-node: Failed to build bindings'); + console.log('@sentry/profiling-node:', clean(spawn.stderr)); + return; + } +} + +if (fs.existsSync(binaries.target)) { + try { + console.log(`@sentry/profiling-node: Precompiled binary found, attempting to load ${binaries.target}`); + require(binaries.target); + console.log('@sentry/profiling-node: Precompiled binary found, skipping build from source.'); + } catch (e) { + console.log('@sentry/profiling-node: Precompiled binary found but failed loading'); + console.log('@sentry/profiling-node:', e); + try { + recompileFromSource(); + } catch (e) { + console.log('@sentry/profiling-node: Failed to compile from source'); + throw e; + } + } +} else { + console.log('@sentry/profiling-node: No precompiled binary found'); + recompileFromSource(); +} diff --git a/packages/profiling-node/scripts/copy-target.js b/packages/profiling-node/scripts/copy-target.js new file mode 100644 index 000000000000..ee3b75163724 --- /dev/null +++ b/packages/profiling-node/scripts/copy-target.js @@ -0,0 +1,27 @@ +// This is a build scripts, so some logging is desireable as it allows +// us to follow the code path that triggered the error. +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const process = require('process'); +const binaries = require('./binaries.js'); + +const build = path.resolve(__dirname, '..', 'lib'); + +if (!fs.existsSync(build)) { + fs.mkdirSync(build, { recursive: true }); +} + +const source = path.join(__dirname, '..', 'build', 'Release', 'sentry_cpu_profiler.node'); +const target = path.join(__dirname, '..', 'lib', binaries.getModuleName()); + +if (!fs.existsSync(source)) { + console.log('Source file does not exist:', source); + process.exit(1); +} else { + if (fs.existsSync(target)) { + console.log('Target file already exists, overwriting it'); + } + console.log('Renaming', source, 'to', target); + fs.renameSync(source, target); +} diff --git a/packages/profiling-node/scripts/prune-profiler-binaries.js b/packages/profiling-node/scripts/prune-profiler-binaries.js new file mode 100755 index 000000000000..925cedaee73a --- /dev/null +++ b/packages/profiling-node/scripts/prune-profiler-binaries.js @@ -0,0 +1,189 @@ +#! /usr/bin/env node + +// This is a build scripts, so some logging is desireable as it allows +// us to follow the code path that triggered the error. +/* eslint-disable no-console */ +const fs = require('fs'); + +let SOURCE_DIR, PLATFORM, ARCH, STDLIB, NODE, HELP; + +for (let i = 0; i < process.argv.length; i++) { + const arg = process.argv[i]; + if (arg.startsWith('--target_dir_path=')) { + SOURCE_DIR = arg.split('=')[1]; + continue; + } + + if (arg.startsWith('--target_platform=')) { + PLATFORM = arg.split('=')[1]; + continue; + } + + if (arg.startsWith('--target_arch=')) { + ARCH = arg.split('=')[1]; + continue; + } + + if (arg.startsWith('--target_stdlib=')) { + STDLIB = arg.split('=')[1]; + continue; + } + + if (arg.startsWith('--target_node=')) { + NODE = arg.split('=')[1]; + continue; + } + + if (arg === '--help' || arg === '-h') { + HELP = true; + continue; + } +} + +if (HELP) { + console.log( + `\nSentry: Prune profiler binaries\n +Usage: sentry-prune-profiler-binaries --target_dir_path=... --target_platform=... --target_arch=... --target_stdlib=...\n +Arguments:\n +--target_dir_path: Path to the directory containing the final bundled code. If you are using webpack, this would be the equivalent of output.path option.\n +--target_node: The major node version the code will be running on. Example: 16, 18, 20...\n +--target_platform: The platform the code will be running on. Example: linux, darwin, win32\n +--target_arch: The architecture the code will be running on. Example: x64, arm64\n +--target_stdlib: The standard library the code will be running on. Example: glibc, musl\n +--dry-run: Do not delete any files, just print the files that would be deleted.\n +--help: Print this help message.\n`, + ); + process.exit(0); +} + +const ARGV_ERRORS = []; + +const NODE_TO_ABI = { + 16: '93', + 18: '108', + 20: '115', +}; + +if (NODE) { + if (NODE_TO_ABI[NODE]) { + NODE = NODE_TO_ABI[NODE]; + } else if (NODE.startsWith('16')) { + NODE = NODE_TO_ABI['16']; + } else if (NODE.startsWith('18')) { + NODE = NODE_TO_ABI['18']; + } else if (NODE.startsWith('20')) { + NODE = NODE_TO_ABI['20']; + } else { + ARGV_ERRORS.push( + '❌ Sentry: Invalid node version passed as argument, please make sure --target_node is a valid major node version. Supported versions are 16, 18 and 20.', + ); + } +} + +if (!SOURCE_DIR) { + ARGV_ERRORS.push( + '❌ Sentry: Missing target_dir_path argument. target_dir_path should point to the directory containing the final bundled code. If you are using webpack, this would be the equivalent of output.path option.', + ); +} + +if (!PLATFORM && !ARCH && !STDLIB) { + ARGV_ERRORS.push( + `❌ Sentry: Missing argument values, pruning requires either --target_platform, --target_arch or --targer_stdlib to be passed as argument values.\n Example: sentry-prune-profiler-binaries --target_platform=linux --target_arch=x64 --target_stdlib=glibc\n +If you are unsure about the execution environment, you can opt to skip some values, but at least one value must be passed.`, + ); +} + +if (ARGV_ERRORS.length > 0) { + console.log(ARGV_ERRORS.join('\n')); + process.exit(1); +} + +const SENTRY__PROFILER_BIN_REGEXP = /sentry_cpu_profiler-.*\.node$/; + +async function findSentryProfilerBinaries(source_dir) { + const binaries = new Set(); + const queue = [source_dir]; + + while (queue.length > 0) { + const dir = queue.pop(); + + for (const file of fs.readdirSync(dir)) { + if (SENTRY__PROFILER_BIN_REGEXP.test(file)) { + binaries.add(`${dir}/${file}`); + continue; + } + + if (fs.statSync(`${dir}/${file}`).isDirectory()) { + if (file === 'node_modules') { + continue; + } + + queue.push(`${dir}/${file}`); + } + } + } + + return binaries; +} + +function bytesToHumanReadable(bytes) { + if (bytes < 1024) { + return `${bytes} Bytes`; + } else if (bytes < 1048576) { + return `${(bytes / 1024).toFixed(2)} KiB`; + } else { + return `${(bytes / 1048576).toFixed(2)} MiB`; + } +} + +async function prune(binaries) { + let bytesSaved = 0; + let removedBinariesCount = 0; + + const conditions = [PLATFORM, ARCH, STDLIB, NODE].filter(n => !!n); + + for (const binary of binaries) { + if (conditions.every(condition => binary.includes(condition))) { + continue; + } + + const stats = fs.statSync(binary); + bytesSaved += stats.size; + removedBinariesCount++; + + if (process.argv.includes('--dry-run')) { + console.log(`Sentry: would have pruned ${binary} (${bytesToHumanReadable(stats.size)})`); + continue; + } + + console.log(`Sentry: pruned ${binary} (${bytesToHumanReadable(stats.size)})`); + fs.unlinkSync(binary); + } + + if (removedBinariesCount === 0) { + console.log( + '❌ Sentry: no binaries pruned, please make sure target argument values are valid or use --help for more information.', + ); + return; + } + + if (process.argv.includes('--dry-run')) { + console.log( + `✅ Sentry: would have pruned ${removedBinariesCount} ${ + removedBinariesCount === 1 ? 'binary' : 'binaries' + } and saved ${bytesToHumanReadable(bytesSaved)}.`, + ); + return; + } + + console.log( + `✅ Sentry: pruned ${removedBinariesCount} ${ + removedBinariesCount === 1 ? 'binary' : 'binaries' + }, saved ${bytesToHumanReadable(bytesSaved)} in total.`, + ); +} + +(async () => { + const binaries = await findSentryProfilerBinaries(SOURCE_DIR); + await prune(binaries); +})(); diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts new file mode 100644 index 000000000000..e4ee11fee6a4 --- /dev/null +++ b/packages/profiling-node/src/cpu_profiler.ts @@ -0,0 +1,154 @@ +import { arch as _arch, platform as _platform } from 'os'; +import { join, resolve } from 'path'; +import { familySync } from 'detect-libc'; +import { getAbi } from 'node-abi'; +import { env, versions } from 'process'; +import { threadId } from 'worker_threads'; + +import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { DEBUG_BUILD } from './debug-build'; +import type { PrivateV8CpuProfilerBindings, V8CpuProfilerBindings } from './types'; + +const stdlib = familySync(); +const platform = process.env['BUILD_PLATFORM'] || _platform(); +const arch = process.env['BUILD_ARCH'] || _arch(); +const abi = getAbi(versions.node, 'node'); +const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); + +const built_from_source_path = resolve(__dirname, `./sentry_cpu_profiler-${identifier}`); + +/** + * Imports cpp bindings based on the current platform and architecture. + */ +// eslint-disable-next-line complexity +export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { + // If a binary path is specified, use that. + if (env['SENTRY_PROFILER_BINARY_PATH']) { + const envPath = env['SENTRY_PROFILER_BINARY_PATH']; + return require(envPath); + } + + // If a user specifies a different binary dir, they are in control of the binaries being moved there + if (env['SENTRY_PROFILER_BINARY_DIR']) { + const binaryPath = join(resolve(env['SENTRY_PROFILER_BINARY_DIR']), `sentry_cpu_profiler-${identifier}`); + return require(`${binaryPath}.node`); + } + + /* eslint-disable no-fallthrough */ + // We need the fallthrough so that in the end, we can fallback to the require dynamice require. + // This is for cases where precompiled binaries were not provided, but may have been compiled from source. + if (platform === 'darwin') { + if (arch === 'x64') { + if (abi === '93') { + return require('./sentry_cpu_profiler-darwin-x64-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-darwin-x64-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-darwin-x64-115.node'); + } + } + + if (arch === 'arm64') { + if (abi === '93') { + return require('./sentry_cpu_profiler-darwin-arm64-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-darwin-arm64-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-darwin-arm64-115.node'); + } + } + } + + if (platform === 'win32') { + if (arch === 'x64') { + if (abi === '93') { + return require('./sentry_cpu_profiler-win32-x64-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-win32-x64-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-win32-x64-115.node'); + } + } + } + + if (platform === 'linux') { + if (arch === 'x64') { + if (stdlib === 'musl') { + if (abi === '93') { + return require('./sentry_cpu_profiler-linux-x64-musl-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-linux-x64-musl-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-linux-x64-musl-115.node'); + } + } + if (stdlib === 'glibc') { + if (abi === '93') { + return require('./sentry_cpu_profiler-linux-x64-glibc-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-linux-x64-glibc-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-linux-x64-glibc-115.node'); + } + } + } + if (arch === 'arm64') { + if (stdlib === 'musl') { + if (abi === '93') { + return require('./sentry_cpu_profiler-linux-arm64-musl-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-linux-arm64-musl-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-linux-arm64-musl-115.node'); + } + } + if (stdlib === 'glibc') { + if (abi === '93') { + return require('./sentry_cpu_profiler-linux-arm64-glibc-93.node'); + } + if (abi === '108') { + return require('./sentry_cpu_profiler-linux-arm64-glibc-108.node'); + } + if (abi === '115') { + return require('./sentry_cpu_profiler-linux-arm64-glibc-115.node'); + } + } + } + } + return require(`${built_from_source_path}.node`); +} + +const PrivateCpuProfilerBindings: PrivateV8CpuProfilerBindings = importCppBindingsModule(); +const CpuProfilerBindings: V8CpuProfilerBindings = { + startProfiling(name: string) { + if (!PrivateCpuProfilerBindings) { + DEBUG_BUILD && logger.log('[Profiling] Bindings not loaded, ignoring call to startProfiling.'); + return; + } + + return PrivateCpuProfilerBindings.startProfiling(name); + }, + stopProfiling(name: string) { + if (!PrivateCpuProfilerBindings) { + DEBUG_BUILD && + logger.log('[Profiling] Bindings not loaded or profile was never started, ignoring call to stopProfiling.'); + return null; + } + return PrivateCpuProfilerBindings.stopProfiling(name, threadId, !!GLOBAL_OBJ._sentryDebugIds); + }, +}; + +export { PrivateCpuProfilerBindings }; +export { CpuProfilerBindings }; diff --git a/packages/profiling-node/src/debug-build.ts b/packages/profiling-node/src/debug-build.ts new file mode 100644 index 000000000000..60aa50940582 --- /dev/null +++ b/packages/profiling-node/src/debug-build.ts @@ -0,0 +1,8 @@ +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/profiling-node/src/hubextensions.ts b/packages/profiling-node/src/hubextensions.ts new file mode 100644 index 000000000000..66a43d3fcb28 --- /dev/null +++ b/packages/profiling-node/src/hubextensions.ts @@ -0,0 +1,253 @@ +import { getMainCarrier } from '@sentry/core'; +import type { NodeClient } from '@sentry/node'; +import type { CustomSamplingContext, Hub, Transaction, TransactionContext } from '@sentry/types'; +import { logger, uuid4 } from '@sentry/utils'; + +import { CpuProfilerBindings } from './cpu_profiler'; +import { DEBUG_BUILD } from './debug-build'; +import { isValidSampleRate } from './utils'; + +export const MAX_PROFILE_DURATION_MS = 30 * 1000; + +type StartTransaction = ( + this: Hub, + transactionContext: TransactionContext, + customSamplingContext?: CustomSamplingContext, +) => Transaction; + +/** + * Takes a transaction and determines if it should be profiled or not. If it should be profiled, it returns the + * profile_id, otherwise returns undefined. Takes care of setting profile context on transaction as well + */ +export function maybeProfileTransaction( + client: NodeClient | undefined, + transaction: Transaction, + customSamplingContext?: CustomSamplingContext, +): string | undefined { + // profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate. We dont perform + // the actual multiplication to get the final rate, but we discard the profile if the transaction was sampled, + // so anything after this block from here is based on the transaction sampling. + // eslint-disable-next-line deprecation/deprecation + if (!transaction.sampled) { + return; + } + + // Client and options are required for profiling + if (!client) { + DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no client found.'); + return; + } + + const options = client.getOptions(); + if (!options) { + DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no options found.'); + return; + } + + const profilesSampler = options.profilesSampler; + let profilesSampleRate: number | boolean | undefined = options.profilesSampleRate; + + // Prefer sampler to sample rate if both are provided. + if (typeof profilesSampler === 'function') { + // eslint-disable-next-line deprecation/deprecation + profilesSampleRate = profilesSampler({ transactionContext: transaction.toContext(), ...customSamplingContext }); + } + + // 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(profilesSampleRate)) { + DEBUG_BUILD && logger.warn('[Profiling] Discarding profile because of invalid sample rate.'); + return; + } + + // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped + if (!profilesSampleRate) { + DEBUG_BUILD && + logger.log( + `[Profiling] Discarding profile because ${ + typeof profilesSampler === 'function' + ? 'profileSampler returned 0 or false' + : 'a negative sampling decision was inherited or profileSampleRate is set to 0' + }`, + ); + return; + } + + // 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 sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate; + // Check if we should sample this profile + if (!sampled) { + DEBUG_BUILD && + logger.log( + `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number( + profilesSampleRate, + )})`, + ); + return; + } + + const profile_id = uuid4(); + CpuProfilerBindings.startProfiling(profile_id); + DEBUG_BUILD && + // eslint-disable-next-line deprecation/deprecation + logger.log(`[Profiling] started profiling transaction: ${transaction.name}`); + + // set transaction context - do this regardless if profiling fails down the line + // so that we can still see the profile_id in the transaction context + return profile_id; +} + +/** + * Stops the profiler for profile_id and returns the profile + * @param transaction + * @param profile_id + * @returns + */ +export function stopTransactionProfile( + transaction: Transaction, + profile_id: string | undefined, +): ReturnType<(typeof CpuProfilerBindings)['stopProfiling']> | null { + // Should not happen, but satisfy the type checker and be safe regardless. + if (!profile_id) { + return null; + } + + const profile = CpuProfilerBindings.stopProfiling(profile_id); + + DEBUG_BUILD && + // eslint-disable-next-line deprecation/deprecation + logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name}`); + + // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. + if (!profile) { + DEBUG_BUILD && + logger.log( + // eslint-disable-next-line deprecation/deprecation + `[Profiling] profiler returned null profile for: ${transaction.name}`, + 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started', + ); + return null; + } + + // Assign profile_id to the profile + profile.profile_id = profile_id; + return profile; +} + +/** + * Wraps startTransaction and stopTransaction with profiling related logic. + * startProfiling is called after the call to startTransaction in order to avoid our own code from + * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction. + */ +export function __PRIVATE__wrapStartTransactionWithProfiling(startTransaction: StartTransaction): StartTransaction { + return function wrappedStartTransaction( + this: Hub, + transactionContext: TransactionContext, + customSamplingContext?: CustomSamplingContext, + ): Transaction { + const transaction: Transaction = startTransaction.call(this, transactionContext, customSamplingContext); + + // Client is required if we want to profile + // eslint-disable-next-line deprecation/deprecation + const client = this.getClient() as NodeClient | undefined; + if (!client) { + return transaction; + } + + // Check if we should profile this transaction. If a profile_id is returned, then profiling has been started. + const profile_id = maybeProfileTransaction(client, transaction, customSamplingContext); + if (!profile_id) { + return transaction; + } + + // A couple of important things to note here: + // `CpuProfilerBindings.stopProfiling` will be scheduled to run in 30seconds in order to exceed max profile duration. + // Whichever of the two (transaction.finish/timeout) is first to run, the profiling will be stopped and the gathered profile + // will be processed when the original transaction is finished. Since onProfileHandler can be invoked multiple times in the + // event of an error or user mistake (calling transaction.finish multiple times), it is important that the behavior of onProfileHandler + // is idempotent as we do not want any timings or profiles to be overriden by the last call to onProfileHandler. + // After the original finish method is called, the event will be reported through the integration and delegated to transport. + let profile: ReturnType<(typeof CpuProfilerBindings)['stopProfiling']> | null = null; + + const options = client.getOptions(); + // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that + // currently exceed the default timeout set by the SDKs. + const maxProfileDurationMs = + (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS; + + // Enqueue a timeout to prevent profiles from running over max duration. + let maxDurationTimeoutID: NodeJS.Timeout | void = global.setTimeout(() => { + DEBUG_BUILD && + // eslint-disable-next-line deprecation/deprecation + logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', transaction.name); + + profile = stopTransactionProfile(transaction, profile_id); + }, maxProfileDurationMs); + + // We need to reference the original finish call to avoid creating an infinite loop + // eslint-disable-next-line deprecation/deprecation + const originalFinish = transaction.finish.bind(transaction); + + // Wrap the transaction finish method to stop profiling and set the profile on the transaction. + function profilingWrappedTransactionFinish(): void { + if (!profile_id) { + return originalFinish(); + } + + // We stop the handler first to ensure that the timeout is cleared and the profile is stopped. + if (maxDurationTimeoutID) { + global.clearTimeout(maxDurationTimeoutID); + maxDurationTimeoutID = undefined; + } + + // onProfileHandler should always return the same profile even if this is called multiple times. + // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared. + if (!profile) { + profile = stopTransactionProfile(transaction, profile_id); + } + + // @ts-expect-error profile is not part of metadata + // eslint-disable-next-line deprecation/deprecation + transaction.setMetadata({ profile }); + return originalFinish(); + } + + // eslint-disable-next-line deprecation/deprecation + transaction.finish = profilingWrappedTransactionFinish; + return transaction; + }; +} + +/** + * Patches startTransaction and stopTransaction with profiling logic. + * This is used by the SDK's that do not support event hooks. + * @private + */ +function _addProfilingExtensionMethods(): void { + const carrier = getMainCarrier(); + if (!carrier.__SENTRY__) { + DEBUG_BUILD && logger.log("[Profiling] Can't find main carrier, profiling won't work."); + return; + } + + carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {}; + if (!carrier.__SENTRY__.extensions['startTransaction']) { + DEBUG_BUILD && logger.log('[Profiling] startTransaction does not exists, profiling will not work.'); + return; + } + + DEBUG_BUILD && logger.log('[Profiling] startTransaction exists, patching it with profiling functionality...'); + + carrier.__SENTRY__.extensions['startTransaction'] = __PRIVATE__wrapStartTransactionWithProfiling( + // This is patched by sentry/tracing, we are going to re-patch it... + carrier.__SENTRY__.extensions['startTransaction'] as StartTransaction, + ); +} + +/** + * This patches the global object and injects the Profiling extensions methods + */ +export function addProfilingExtensionMethods(): void { + _addProfilingExtensionMethods(); +} diff --git a/packages/profiling-node/src/index.ts b/packages/profiling-node/src/index.ts new file mode 100644 index 000000000000..fee7c526929d --- /dev/null +++ b/packages/profiling-node/src/index.ts @@ -0,0 +1 @@ +export { ProfilingIntegration } from './integration'; diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts new file mode 100644 index 000000000000..28bf24f0d784 --- /dev/null +++ b/packages/profiling-node/src/integration.ts @@ -0,0 +1,245 @@ +import type { NodeClient } from '@sentry/node'; +import type { Event, EventProcessor, Hub, Integration, Transaction } from '@sentry/types'; + +import { logger } from '@sentry/utils'; + +import { DEBUG_BUILD } from './debug-build'; +import { + MAX_PROFILE_DURATION_MS, + addProfilingExtensionMethods, + maybeProfileTransaction, + stopTransactionProfile, +} from './hubextensions'; +import type { Profile, RawThreadCpuProfile } from './types'; + +import { + addProfilesToEnvelope, + createProfilingEvent, + createProfilingEventEnvelope, + findProfiledTransactionsFromEnvelope, + isProfiledTransactionEvent, + maybeRemoveProfileFromSdkMetadata, +} from './utils'; + +const MAX_PROFILE_QUEUE_LENGTH = 50; +const PROFILE_QUEUE: RawThreadCpuProfile[] = []; +const PROFILE_TIMEOUTS: Record = {}; + +function addToProfileQueue(profile: RawThreadCpuProfile): void { + PROFILE_QUEUE.push(profile); + + // We only want to keep the last n profiles in the queue. + if (PROFILE_QUEUE.length > MAX_PROFILE_QUEUE_LENGTH) { + PROFILE_QUEUE.shift(); + } +} + +/** + * We need this integration in order to send data to Sentry. We hook into the event processor + * and inspect each event to see if it is a transaction event and if that transaction event + * contains a profile on it's metadata. If that is the case, we create a profiling event envelope + * and delete the profile from the transaction metadata. + */ +export class ProfilingIntegration implements Integration { + /** + * @inheritDoc + */ + public readonly name: string; + public getCurrentHub?: () => Hub; + + public constructor() { + this.name = 'ProfilingIntegration'; + } + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + this.getCurrentHub = getCurrentHub; + // eslint-disable-next-line deprecation/deprecation + const client = this.getCurrentHub().getClient() as NodeClient; + + if (client && typeof client.on === 'function') { + client.on('startTransaction', (transaction: Transaction) => { + const profile_id = maybeProfileTransaction(client, transaction, undefined); + + if (profile_id) { + const options = client.getOptions(); + // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that + // currently exceed the default timeout set by the SDKs. + const maxProfileDurationMs = + (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS; + + if (PROFILE_TIMEOUTS[profile_id]) { + global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete PROFILE_TIMEOUTS[profile_id]; + } + + // Enqueue a timeout to prevent profiles from running over max duration. + PROFILE_TIMEOUTS[profile_id] = global.setTimeout(() => { + DEBUG_BUILD && + // eslint-disable-next-line deprecation/deprecation + logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', transaction.name); + + const profile = stopTransactionProfile(transaction, profile_id); + if (profile) { + addToProfileQueue(profile); + } + }, maxProfileDurationMs); + + // eslint-disable-next-line deprecation/deprecation + transaction.setContext('profile', { profile_id }); + // @ts-expect-error profile_id is not part of the metadata type + // eslint-disable-next-line deprecation/deprecation + transaction.setMetadata({ profile_id: profile_id }); + } + }); + + client.on('finishTransaction', transaction => { + // @ts-expect-error profile_id is not part of the metadata type + // eslint-disable-next-line deprecation/deprecation + const profile_id = transaction.metadata.profile_id; + if (profile_id && typeof profile_id === 'string') { + if (PROFILE_TIMEOUTS[profile_id]) { + global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete PROFILE_TIMEOUTS[profile_id]; + } + const profile = stopTransactionProfile(transaction, profile_id); + + if (profile) { + addToProfileQueue(profile); + } + } + }); + + client.on('beforeEnvelope', (envelope): void => { + // if not profiles are in queue, there is nothing to add to the envelope. + if (!PROFILE_QUEUE.length) { + return; + } + + const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); + if (!profiledTransactionEvents.length) { + return; + } + + const profilesToAddToEnvelope: Profile[] = []; + + for (const profiledTransaction of profiledTransactionEvents) { + const profileContext = profiledTransaction.contexts?.['profile']; + const profile_id = profileContext?.['profile_id']; + + if (!profile_id) { + throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context'); + } + + // Remove the profile from the transaction context before sending, relay will take care of the rest. + if (profileContext) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete profiledTransaction.contexts?.['profile']; + } + + // We need to find both a profile and a transaction event for the same profile_id. + const profileIndex = PROFILE_QUEUE.findIndex(p => p.profile_id === profile_id); + if (profileIndex === -1) { + DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + continue; + } + + const cpuProfile = PROFILE_QUEUE[profileIndex]; + if (!cpuProfile) { + DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + continue; + } + + // Remove the profile from the queue. + PROFILE_QUEUE.splice(profileIndex, 1); + const profile = createProfilingEvent(cpuProfile, profiledTransaction); + + if (client.emit && profile) { + const integrations = + client['_integrations'] && client['_integrations'] !== null && !Array.isArray(client['_integrations']) + ? Object.keys(client['_integrations']) + : undefined; + + // @ts-expect-error bad overload due to unknown event + client.emit('preprocessEvent', profile, { + event_id: profiledTransaction.event_id, + integrations, + }); + } + + if (profile) { + profilesToAddToEnvelope.push(profile); + } + } + + addProfilesToEnvelope(envelope, profilesToAddToEnvelope); + }); + } else { + // Patch the carrier methods and add the event processor. + addProfilingExtensionMethods(); + addGlobalEventProcessor(this.handleGlobalEvent.bind(this)); + } + } + + /** + * @inheritDoc + */ + public async handleGlobalEvent(event: Event): Promise { + if (this.getCurrentHub === undefined) { + return maybeRemoveProfileFromSdkMetadata(event); + } + + if (isProfiledTransactionEvent(event)) { + // Client, Dsn and Transport are all required to be able to send the profiling event to Sentry. + // If either of them is not available, we remove the profile from the transaction event. + // and forward it to the next event processor. + const hub = this.getCurrentHub(); + + // eslint-disable-next-line deprecation/deprecation + const client = hub.getClient(); + + if (!client) { + DEBUG_BUILD && + logger.log( + '[Profiling] getClient did not return a Client, removing profile from event and forwarding to next event processors.', + ); + return maybeRemoveProfileFromSdkMetadata(event); + } + + const dsn = client.getDsn(); + if (!dsn) { + DEBUG_BUILD && + logger.log( + '[Profiling] getDsn did not return a Dsn, removing profile from event and forwarding to next event processors.', + ); + return maybeRemoveProfileFromSdkMetadata(event); + } + + const transport = client.getTransport(); + if (!transport) { + DEBUG_BUILD && + logger.log( + '[Profiling] getTransport did not return a Transport, removing profile from event and forwarding to next event processors.', + ); + return maybeRemoveProfileFromSdkMetadata(event); + } + + // If all required components are available, we construct a profiling event envelope and send it to Sentry. + DEBUG_BUILD && logger.log('[Profiling] Preparing envelope and sending a profiling event'); + const envelope = createProfilingEventEnvelope(event, dsn); + + if (envelope) { + // Fire and forget, we don't want to block the main event processing flow. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + transport.send(envelope); + } + } + + // Ensure sdkProcessingMetadata["profile"] is removed from the event before forwarding it to the next event processor. + return maybeRemoveProfileFromSdkMetadata(event); + } +} diff --git a/packages/profiling-node/src/types.ts b/packages/profiling-node/src/types.ts new file mode 100644 index 000000000000..3042335269eb --- /dev/null +++ b/packages/profiling-node/src/types.ts @@ -0,0 +1,105 @@ +import type { Event } from '@sentry/types'; + +interface Sample { + stack_id: number; + thread_id: string; + elapsed_since_start_ns: string; +} + +type Stack = number[]; + +type Frame = { + function: string; + file: string; + lineno: number; + colno: number; +}; + +interface Measurement { + unit: string; + values: { + elapsed_since_start_ns: number; + value: number; + }[]; +} + +export interface DebugImage { + code_file: string; + type: string; + debug_id: string; + image_addr?: string; + image_size?: number; + image_vmaddr?: string; +} + +// Profile is marked as optional because it is deleted from the metadata +// by the integration before the event is processed by other integrations. +export interface ProfiledEvent extends Event { + sdkProcessingMetadata: { + profile?: RawThreadCpuProfile; + }; +} + +export interface RawThreadCpuProfile { + profile_id?: string; + stacks: ReadonlyArray; + samples: ReadonlyArray; + frames: ReadonlyArray; + resources: ReadonlyArray; + profiler_logging_mode: 'eager' | 'lazy'; + measurements: Record; +} +export interface ThreadCpuProfile { + stacks: ReadonlyArray; + samples: ReadonlyArray; + frames: ReadonlyArray; + thread_metadata: Record; + queue_metadata?: Record; +} + +export interface PrivateV8CpuProfilerBindings { + startProfiling(name: string): void; + stopProfiling(name: string, threadId: number, collectResources: boolean): RawThreadCpuProfile | null; + getFrameModule(abs_path: string): string; +} + +export interface V8CpuProfilerBindings { + startProfiling(name: string): void; + stopProfiling(name: string): RawThreadCpuProfile | null; +} + +export interface Profile { + event_id: string; + version: string; + os: { + name: string; + version: string; + build_number: string; + }; + runtime: { + name: string; + version: string; + }; + device: { + architecture: string; + is_emulator: boolean; + locale: string; + manufacturer: string; + model: string; + }; + timestamp: string; + release: string; + environment: string; + platform: string; + profile: ThreadCpuProfile; + debug_meta?: { + images: DebugImage[]; + }; + transaction: { + name: string; + id: string; + trace_id: string; + active_thread_id: string; + }; + measurements: Record; +} diff --git a/packages/profiling-node/src/utils.ts b/packages/profiling-node/src/utils.ts new file mode 100644 index 000000000000..1e34fbfd8974 --- /dev/null +++ b/packages/profiling-node/src/utils.ts @@ -0,0 +1,513 @@ +/* eslint-disable max-lines */ +import * as os from 'os'; +import type { + Context, + DsnComponents, + DynamicSamplingContext, + Envelope, + Event, + EventEnvelope, + EventEnvelopeHeaders, + EventItem, + SdkInfo, + SdkMetadata, + StackFrame, + StackParser, +} from '@sentry/types'; +import { env, versions } from 'process'; +import { isMainThread, threadId } from 'worker_threads'; + +import * as Sentry from '@sentry/node'; +import { GLOBAL_OBJ, createEnvelope, dropUndefinedKeys, dsnToString, forEachEnvelopeItem, logger } from '@sentry/utils'; + +import { DEBUG_BUILD } from './debug-build'; +import type { Profile, ProfiledEvent, RawThreadCpuProfile, ThreadCpuProfile } from './types'; +import type { DebugImage } from './types'; + +// We require the file because if we import it, it will be included in the bundle. +// I guess tsc does not check file contents when it's imported. +// eslint-disable-next-line +const THREAD_ID_STRING = String(threadId); +const THREAD_NAME = isMainThread ? 'main' : 'worker'; +const FORMAT_VERSION = '1'; + +// Os machine was backported to 16.18, but this was not reflected in the types +// @ts-expect-error ignore missing +const machine = typeof os.machine === 'function' ? os.machine() : os.arch(); + +// Machine properties (eval only once) +const PLATFORM = os.platform(); +const RELEASE = os.release(); +const VERSION = os.version(); +const TYPE = os.type(); +const MODEL = machine; +const ARCH = os.arch(); + +/** + * Checks if the profile is a raw profile or a profile enriched with thread information. + * @param {ThreadCpuProfile | RawThreadCpuProfile} profile + * @returns {boolean} + */ +function isRawThreadCpuProfile(profile: ThreadCpuProfile | RawThreadCpuProfile): profile is RawThreadCpuProfile { + return !('thread_metadata' in profile); +} + +/** + * Enriches the profile with threadId of the current thread. + * This is done in node as we seem to not be able to get the info from C native code. + * + * @param {ThreadCpuProfile | RawThreadCpuProfile} profile + * @returns {ThreadCpuProfile} + */ +export function enrichWithThreadInformation(profile: ThreadCpuProfile | RawThreadCpuProfile): ThreadCpuProfile { + if (!isRawThreadCpuProfile(profile)) { + return profile; + } + + return { + samples: profile.samples, + frames: profile.frames, + stacks: profile.stacks, + thread_metadata: { + [THREAD_ID_STRING]: { + name: THREAD_NAME, + }, + }, + }; +} + +/** + * Extract sdk info from from the API metadata + * @param {SdkMetadata | undefined} metadata + * @returns {SdkInfo | undefined} + */ +function getSdkMetadataForEnvelopeHeader(metadata?: SdkMetadata): SdkInfo | undefined { + if (!metadata || !metadata.sdk) { + return undefined; + } + + return { name: metadata.sdk.name, version: metadata.sdk.version } as SdkInfo; +} + +/** + * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. + * Merge with existing data if any. + * + * @param {Event} event + * @param {SdkInfo | undefined} sdkInfo + * @returns {Event} + */ +function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event { + if (!sdkInfo) { + return event; + } + event.sdk = event.sdk || {}; + event.sdk.name = event.sdk.name || sdkInfo.name || 'unknown sdk'; + event.sdk.version = event.sdk.version || sdkInfo.version || 'unknown sdk version'; + event.sdk.integrations = [...(event.sdk.integrations || []), ...(sdkInfo.integrations || [])]; + event.sdk.packages = [...(event.sdk.packages || []), ...(sdkInfo.packages || [])]; + return event; +} + +/** + * + * @param {Event} event + * @param {SdkInfo | undefined} sdkInfo + * @param {string | undefined} tunnel + * @param {DsnComponents} dsn + * @returns {EventEnvelopeHeaders} + */ +function createEventEnvelopeHeaders( + event: Event, + sdkInfo: SdkInfo | undefined, + tunnel: string | undefined, + dsn: DsnComponents, +): EventEnvelopeHeaders { + const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata['dynamicSamplingContext']; + + return { + event_id: event.event_id as string, + sent_at: new Date().toISOString(), + ...(sdkInfo && { sdk: sdkInfo }), + ...(!!tunnel && { dsn: dsnToString(dsn) }), + ...(event.type === 'transaction' && + dynamicSamplingContext && { + trace: dropUndefinedKeys({ ...dynamicSamplingContext }) as DynamicSamplingContext, + }), + }; +} + +/** + * Creates a profiling event envelope from a Sentry event. If profile does not pass + * validation, returns null. + * @param {Event} + * @returns {Profile | null} + */ +export function createProfilingEventFromTransaction(event: ProfiledEvent): Profile | null { + if (event.type !== 'transaction') { + // createProfilingEventEnvelope should only be called for transactions, + // we type guard this behavior with isProfiledTransactionEvent. + throw new TypeError('Profiling events may only be attached to transactions, this should never occur.'); + } + + const rawProfile = event.sdkProcessingMetadata['profile']; + if (rawProfile === undefined || rawProfile === null) { + throw new TypeError( + `Cannot construct profiling event envelope without a valid profile. Got ${rawProfile} instead.`, + ); + } + + if (!rawProfile.profile_id) { + throw new TypeError( + `Cannot construct profiling event envelope without a valid profile id. Got ${rawProfile.profile_id} instead.`, + ); + } + + if (!isValidProfile(rawProfile)) { + return null; + } + + return createProfilePayload(rawProfile, { + release: event.release ?? '', + environment: event.environment ?? '', + event_id: event.event_id ?? '', + transaction: event.transaction ?? '', + start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(), + trace_id: event.contexts?.['trace']?.['trace_id'] ?? '', + profile_id: rawProfile.profile_id, + }); +} + +/** + * Creates a profiling envelope item, if the profile does not pass validation, returns null. + * @param {RawThreadCpuProfile} + * @param {Event} + * @returns {Profile | null} + */ +export function createProfilingEvent(profile: RawThreadCpuProfile, event: Event): Profile | null { + if (!isValidProfile(profile)) { + return null; + } + + return createProfilePayload(profile, { + release: event.release ?? '', + environment: event.environment ?? '', + event_id: event.event_id ?? '', + transaction: event.transaction ?? '', + start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(), + trace_id: event.contexts?.['trace']?.['trace_id'] ?? '', + profile_id: profile.profile_id, + }); +} + +/** + * Create a profile + * @param {RawThreadCpuProfile} cpuProfile + * @param {options} + * @returns {Profile} + */ + +function createProfilePayload( + cpuProfile: RawThreadCpuProfile, + { + release, + environment, + event_id, + transaction, + start_timestamp, + trace_id, + profile_id, + }: { + release: string; + environment: string; + event_id: string; + transaction: string; + start_timestamp: number; + trace_id: string | undefined; + profile_id: string; + }, +): Profile { + // Log a warning if the profile has an invalid traceId (should be uuidv4). + // All profiles and transactions are rejected if this is the case and we want to + // warn users that this is happening if they enable debug flag + if (trace_id && trace_id.length !== 32) { + DEBUG_BUILD && logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`); + } + + const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile); + + const profile: Profile = { + event_id: profile_id, + timestamp: new Date(start_timestamp).toISOString(), + platform: 'node', + version: FORMAT_VERSION, + release: release, + environment: environment, + measurements: cpuProfile.measurements, + runtime: { + name: 'node', + version: versions.node || '', + }, + os: { + name: PLATFORM, + version: RELEASE, + build_number: VERSION, + }, + device: { + locale: env['LC_ALL'] || env['LC_MESSAGES'] || env['LANG'] || env['LANGUAGE'] || '', + model: MODEL, + manufacturer: TYPE, + architecture: ARCH, + is_emulator: false, + }, + debug_meta: { + images: applyDebugMetadata(cpuProfile.resources), + }, + profile: enrichedThreadProfile, + transaction: { + name: transaction, + id: event_id, + trace_id: trace_id || '', + active_thread_id: THREAD_ID_STRING, + }, + }; + + return profile; +} + +/** + * Creates an envelope from a profiling event. + * @param {Event} Profile + * @param {DsnComponents} dsn + * @param {SdkMetadata} metadata + * @param {string|undefined} tunnel + * @returns {Envelope|null} + */ +export function createProfilingEventEnvelope( + event: ProfiledEvent, + dsn: DsnComponents, + metadata?: SdkMetadata, + tunnel?: string, +): EventEnvelope | null { + const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); + enhanceEventWithSdkInfo(event, metadata && metadata.sdk); + + const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn); + const profile = createProfilingEventFromTransaction(event); + + if (!profile) { + return null; + } + + const envelopeItem: EventItem = [ + { + type: 'profile', + }, + // @ts-expect-error profile is not part of EventItem yet + profile, + ]; + + return createEnvelope(envelopeHeaders, [envelopeItem]); +} + +/** + * Check if event metadata contains profile information + * @param {Event} + * @returns {boolean} + */ +export function isProfiledTransactionEvent(event: Event): event is ProfiledEvent { + return !!(event.sdkProcessingMetadata && event.sdkProcessingMetadata['profile']); +} + +/** + * Due to how profiles are attached to event metadata, we may sometimes want to remove them to ensure + * they are not processed by other Sentry integrations. This can be the case when we cannot construct a valid + * profile from the data we have or some of the mechanisms to send the event (Hub, Transport etc) are not available to us. + * + * @param {Event | ProfiledEvent} event + * @returns {Event} + */ +export function maybeRemoveProfileFromSdkMetadata(event: Event | ProfiledEvent): Event { + if (!isProfiledTransactionEvent(event)) { + return event; + } + + delete event.sdkProcessingMetadata.profile; + return event; +} + +/** + * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). + * @param {unknown} rate + * @returns {boolean} + */ +export 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 ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) { + DEBUG_BUILD && + logger.warn( + `[Profiling] Invalid sample rate. 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; + } + + // Boolean sample rates are always valid + if (rate === true || rate === false) { + return true; + } + + // 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(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`); + return false; + } + return true; +} + +/** + * Checks if the profile is valid and can be sent to Sentry. + * @param {RawThreadCpuProfile} profile + * @returns {boolean} + */ +export function isValidProfile(profile: RawThreadCpuProfile): profile is RawThreadCpuProfile & { profile_id: string } { + if (profile.samples.length <= 1) { + DEBUG_BUILD && + // Log a warning if the profile has less than 2 samples so users can know why + // they are not seeing any profiling data and we cant avoid the back and forth + // of asking them to provide us with a dump of the profile data. + logger.log('[Profiling] Discarding profile because it contains less than 2 samples'); + return false; + } + + if (!profile.profile_id) { + return false; + } + + return true; +} + +/** + * Adds items to envelope if they are not already present - mutates the envelope. + * @param {Envelope} envelope + * @param {Profile[]} profiles + * @returns {Envelope} + */ +export function addProfilesToEnvelope(envelope: Envelope, profiles: Profile[]): Envelope { + if (!profiles.length) { + return envelope; + } + + for (const profile of profiles) { + // @ts-expect-error untyped envelope + envelope[1].push([{ type: 'profile' }, profile]); + } + return envelope; +} + +/** + * Finds transactions with profile_id context in the envelope + * @param {Envelope} envelope + * @returns {Event[]} + */ +export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[] { + const events: Event[] = []; + + forEachEnvelopeItem(envelope, (item, type) => { + if (type !== 'transaction') { + return; + } + + // First item is the type, so we can skip it, everything else is an event + for (let j = 1; j < item.length; j++) { + const event = item[j]; + + if (!event) { + // Shouldnt happen, but lets be safe + continue; + } + + // @ts-expect-error profile_id is not part of the metadata type + const profile_id = (event.contexts as Context)?.['profile']?.['profile_id']; + + if (event && profile_id) { + events.push(item[j] as Event); + } + } + }); + + return events; +} + +const debugIdStackParserCache = new WeakMap>(); + +/** + * Cross reference profile collected resources with debug_ids and return a list of debug images. + * @param {string[]} resource_paths + * @returns {DebugImage[]} + */ +export function applyDebugMetadata(resource_paths: ReadonlyArray): DebugImage[] { + const debugIdMap = GLOBAL_OBJ._sentryDebugIds; + + if (!debugIdMap) { + return []; + } + + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + const client = hub.getClient(); + const options = client && client.getOptions(); + + if (!options || !options.stackParser) { + return []; + } + + let debugIdStackFramesCache: Map; + const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(options.stackParser); + if (cachedDebugIdStackFrameCache) { + debugIdStackFramesCache = cachedDebugIdStackFrameCache; + } else { + debugIdStackFramesCache = new Map(); + debugIdStackParserCache.set(options.stackParser, debugIdStackFramesCache); + } + + // Build a map of filename -> debug_id. + const filenameDebugIdMap = Object.keys(debugIdMap).reduce>((acc, debugIdStackTrace) => { + let parsedStack: StackFrame[]; + + const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace); + if (cachedParsedStack) { + parsedStack = cachedParsedStack; + } else { + parsedStack = options.stackParser(debugIdStackTrace); + debugIdStackFramesCache.set(debugIdStackTrace, parsedStack); + } + + for (let i = parsedStack.length - 1; i >= 0; i--) { + const stackFrame = parsedStack[i]; + const file = stackFrame && stackFrame.filename; + + if (stackFrame && file) { + acc[file] = debugIdMap[debugIdStackTrace] as string; + break; + } + } + return acc; + }, {}); + + const images: DebugImage[] = []; + + for (const resource of resource_paths) { + if (resource && filenameDebugIdMap[resource]) { + images.push({ + type: 'sourcemap', + code_file: resource, + debug_id: filenameDebugIdMap[resource] as string, + }); + } + } + + return images; +} diff --git a/packages/profiling-node/test/bindings.test.ts b/packages/profiling-node/test/bindings.test.ts new file mode 100644 index 000000000000..c524a277bfa9 --- /dev/null +++ b/packages/profiling-node/test/bindings.test.ts @@ -0,0 +1,30 @@ +import { platform } from 'os'; +// Contains unit tests for some of the C++ bindings. These functions +// are exported on the private bindings object, so we can test them and +// they should not be used outside of this file. +import { PrivateCpuProfilerBindings } from '../src/cpu_profiler'; + +const cases = [ + ['/Users/jonas/code/node_modules/@scope/package/file.js', '@scope.package:file'], + ['/Users/jonas/code/node_modules/package/dir/file.js', 'package.dir:file'], + ['/Users/jonas/code/node_modules/package/file.js', 'package:file'], + ['/Users/jonas/code/src/file.js', 'Users.jonas.code.src:file'], + + // Preserves non .js extensions + ['/Users/jonas/code/src/file.ts', 'Users.jonas.code.src:file.ts'], + // No extension + ['/Users/jonas/code/src/file', 'Users.jonas.code.src:file'], + // Edge cases that shouldn't happen in practice, but try and handle them so we dont crash + ['/Users/jonas/code/src/file.js', 'Users.jonas.code.src:file'], + ['', ''], +]; + +describe('GetFrameModule', () => { + it.each( + platform() === 'win32' + ? cases.map(([abs_path, expected]) => [abs_path ? `C:${abs_path.replace(/\//g, '\\')}` : '', expected]) + : cases, + )('%s => %s', (abs_path: string, expected: string) => { + expect(PrivateCpuProfilerBindings.getFrameModule(abs_path)).toBe(expected); + }); +}); diff --git a/packages/profiling-node/test/cpu_profiler.test.ts b/packages/profiling-node/test/cpu_profiler.test.ts new file mode 100644 index 000000000000..8f66a91cb5ef --- /dev/null +++ b/packages/profiling-node/test/cpu_profiler.test.ts @@ -0,0 +1,302 @@ +import { CpuProfilerBindings, PrivateCpuProfilerBindings } from '../src/cpu_profiler'; +import type { RawThreadCpuProfile, ThreadCpuProfile } from '../src/types'; + +// Required because we test a hypothetical long profile +// and we cannot use advance timers as the c++ relies on +// actual event loop ticks that we cannot advance from jest. +jest.setTimeout(60_000); + +function fail(message: string): never { + throw new Error(message); +} + +const fibonacci = (n: number): number => { + if (n <= 1) { + return n; + } + return fibonacci(n - 1) + fibonacci(n - 2); +}; + +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const profiled = async (name: string, fn: () => void) => { + CpuProfilerBindings.startProfiling(name); + await fn(); + return CpuProfilerBindings.stopProfiling(name); +}; + +const assertValidSamplesAndStacks = (stacks: ThreadCpuProfile['stacks'], samples: ThreadCpuProfile['samples']) => { + expect(stacks.length).toBeGreaterThan(0); + expect(samples.length).toBeGreaterThan(0); + expect(stacks.length <= samples.length).toBe(true); + + for (const sample of samples) { + if (sample.stack_id === undefined) { + throw new Error(`Sample ${JSON.stringify(sample)} has not stack id associated`); + } + if (!stacks[sample.stack_id]) { + throw new Error(`Failed to find stack for sample: ${JSON.stringify(sample)}`); + } + expect(stacks[sample.stack_id]).not.toBe(undefined); + } + + for (const stack of stacks) { + expect(stack).not.toBe(undefined); + } +}; + +const isValidMeasurementValue = (v: any) => { + if (isNaN(v)) return false; + return typeof v === 'number' && v > 0; +}; + +const assertValidMeasurements = (measurement: RawThreadCpuProfile['measurements']['memory_footprint'] | undefined) => { + if (!measurement) { + throw new Error('Measurement is undefined'); + } + expect(measurement).not.toBe(undefined); + expect(typeof measurement.unit).toBe('string'); + expect(measurement.unit.length).toBeGreaterThan(0); + + for (let i = 0; i < measurement.values.length; i++) { + expect(measurement?.values?.[i]?.elapsed_since_start_ns).toBeGreaterThan(0); + expect(measurement?.values?.[i]?.value).toBeGreaterThan(0); + } +}; + +describe('Private bindings', () => { + it('does not crash if collect resources is false', async () => { + PrivateCpuProfilerBindings.startProfiling('profiled-program'); + await wait(100); + expect(() => { + const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, false); + if (!profile) throw new Error('No profile'); + }).not.toThrow(); + }); + + it('collects resources', async () => { + PrivateCpuProfilerBindings.startProfiling('profiled-program'); + await wait(100); + + const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, true); + if (!profile) throw new Error('No profile'); + + expect(profile.resources.length).toBeGreaterThan(0); + + expect(new Set(profile.resources).size).toBe(profile.resources.length); + + for (const resource of profile.resources) { + expect(typeof resource).toBe('string'); + expect(resource).not.toBe(undefined); + } + }); + + it('does not collect resources', async () => { + PrivateCpuProfilerBindings.startProfiling('profiled-program'); + await wait(100); + + const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, false); + if (!profile) throw new Error('No profile'); + + expect(profile.resources.length).toBe(0); + }); +}); + +describe('Profiler bindings', () => { + it('exports profiler binding methods', () => { + expect(typeof CpuProfilerBindings['startProfiling']).toBe('function'); + expect(typeof CpuProfilerBindings['stopProfiling']).toBe('function'); + }); + + it('profiles a program', async () => { + const profile = await profiled('profiled-program', async () => { + await wait(100); + }); + + if (!profile) fail('Profile is null'); + + assertValidSamplesAndStacks(profile.stacks, profile.samples); + }); + + it('adds thread_id info', async () => { + const profile = await profiled('profiled-program', async () => { + await wait(100); + }); + + if (!profile) fail('Profile is null'); + const samples = profile.samples; + + if (!samples.length) { + throw new Error('No samples'); + } + for (const sample of samples) { + expect(sample.thread_id).toBe('0'); + } + }); + + it('caps stack depth at 128', async () => { + const recurseToDepth = async (depth: number): Promise => { + if (depth === 0) { + // Wait a bit to make sure stack gets sampled here + await wait(1000); + return 0; + } + const v = await recurseToDepth(depth - 1); + return v; + }; + + const profile = await profiled('profiled-program', async () => { + await recurseToDepth(256); + }); + + if (!profile) fail('Profile is null'); + + for (const stack of profile.stacks) { + expect(stack.length).toBeLessThanOrEqual(128); + } + }); + + it('does not record two profiles when titles match', () => { + CpuProfilerBindings.startProfiling('same-title'); + CpuProfilerBindings.startProfiling('same-title'); + + const first = CpuProfilerBindings.stopProfiling('same-title'); + const second = CpuProfilerBindings.stopProfiling('same-title'); + + expect(first).not.toBe(null); + expect(second).toBe(null); + }); + + it('weird cases', () => { + CpuProfilerBindings.startProfiling('same-title'); + expect(() => { + CpuProfilerBindings.stopProfiling('same-title'); + CpuProfilerBindings.stopProfiling('same-title'); + }).not.toThrow(); + }); + + it('does not crash if stopTransaction is called before startTransaction', () => { + expect(CpuProfilerBindings.stopProfiling('does not exist')).toBe(null); + }); + + it('does crash if name is invalid', () => { + expect(() => CpuProfilerBindings.stopProfiling('')).toThrow(); + // @ts-expect-error test invalid input + expect(() => CpuProfilerBindings.stopProfiling(undefined)).toThrow(); + // @ts-expect-error test invalid input + expect(() => CpuProfilerBindings.stopProfiling(null)).toThrow(); + // @ts-expect-error test invalid input + expect(() => CpuProfilerBindings.stopProfiling({})).toThrow(); + }); + + it('does not throw if stopTransaction is called before startTransaction', () => { + expect(CpuProfilerBindings.stopProfiling('does not exist')).toBe(null); + expect(() => CpuProfilerBindings.stopProfiling('does not exist')).not.toThrow(); + }); + + it('compiles with eager logging by default', async () => { + const profile = await profiled('profiled-program', async () => { + await wait(100); + }); + + if (!profile) fail('Profile is null'); + expect(profile.profiler_logging_mode).toBe('eager'); + }); + + it('stacks are not null', async () => { + const profile = await profiled('non nullable stack', async () => { + await wait(1000); + fibonacci(36); + await wait(1000); + }); + + if (!profile) fail('Profile is null'); + assertValidSamplesAndStacks(profile.stacks, profile.samples); + }); + + it('samples at ~99hz', async () => { + CpuProfilerBindings.startProfiling('profile'); + await wait(100); + const profile = CpuProfilerBindings.stopProfiling('profile'); + + if (!profile) fail('Profile is null'); + + // Exception for macos and windows - we seem to get way less samples there, but I'm not sure if that's due to poor + // performance of the actions runner, machine or something else. This needs more investigation to determine + // the cause of low sample count. https://github.com/actions/runner-images/issues/1336 seems relevant. + if (process.platform === 'darwin' || process.platform === 'win32') { + if (profile.samples.length < 2) { + fail(`Only ${profile.samples.length} samples obtained on ${process.platform}, expected at least 2`); + } + } else { + if (profile.samples.length < 6) { + fail(`Only ${profile.samples.length} samples obtained on ${process.platform}, expected at least 6`); + } + } + if (profile.samples.length > 15) { + fail(`Too many samples on ${process.platform}, got ${profile.samples.length}`); + } + }); + + it('collects memory footprint', async () => { + CpuProfilerBindings.startProfiling('profile'); + await wait(1000); + const profile = CpuProfilerBindings.stopProfiling('profile'); + + const heap_usage = profile?.measurements['memory_footprint']; + if (!heap_usage) { + throw new Error('memory_footprint is null'); + } + expect(heap_usage.values.length).toBeGreaterThan(6); + expect(heap_usage.values.length).toBeLessThanOrEqual(11); + expect(heap_usage.unit).toBe('byte'); + expect(heap_usage.values.every(v => isValidMeasurementValue(v.value))).toBe(true); + assertValidMeasurements(profile.measurements['memory_footprint']); + }); + + it('collects cpu usage', async () => { + CpuProfilerBindings.startProfiling('profile'); + await wait(1000); + const profile = CpuProfilerBindings.stopProfiling('profile'); + + const cpu_usage = profile?.measurements['cpu_usage']; + if (!cpu_usage) { + throw new Error('cpu_usage is null'); + } + expect(cpu_usage.values.length).toBeGreaterThan(6); + expect(cpu_usage.values.length).toBeLessThanOrEqual(11); + expect(cpu_usage.values.every(v => isValidMeasurementValue(v.value))).toBe(true); + expect(cpu_usage.unit).toBe('percent'); + assertValidMeasurements(profile.measurements['cpu_usage']); + }); + + it('does not overflow measurement buffer if profile runs longer than 30s', async () => { + CpuProfilerBindings.startProfiling('profile'); + await wait(35000); + const profile = CpuProfilerBindings.stopProfiling('profile'); + expect(profile).not.toBe(null); + expect(profile?.measurements?.['cpu_usage']?.values.length).toBeLessThanOrEqual(300); + expect(profile?.measurements?.['memory_footprint']?.values.length).toBeLessThanOrEqual(300); + }); + + // eslint-disable-next-line jest/no-disabled-tests + it.skip('includes deopt reason', async () => { + // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#52-the-object-being-iterated-is-not-a-simple-enumerable + function iterateOverLargeHashTable() { + const table: Record = {}; + for (let i = 0; i < 1e5; i++) { + table[i] = i; + } + // eslint-disable-next-line + for (const _ in table) { + } + } + + const profile = await profiled('profiled-program', async () => { + iterateOverLargeHashTable(); + }); + + // @ts-expect-error deopt reasons are disabled for now as we need to figure out the backend support + const hasDeoptimizedFrame = profile.frames.some(f => f.deopt_reasons && f.deopt_reasons.length > 0); + expect(hasDeoptimizedFrame).toBe(true); + }); +}); diff --git a/packages/profiling-node/test/hubextensions.hub.test.ts b/packages/profiling-node/test/hubextensions.hub.test.ts new file mode 100644 index 000000000000..954f3300ffca --- /dev/null +++ b/packages/profiling-node/test/hubextensions.hub.test.ts @@ -0,0 +1,481 @@ +import * as Sentry from '@sentry/node'; + +import { getMainCarrier } from '@sentry/core'; +import type { Transport } from '@sentry/types'; +import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/utils'; +import { CpuProfilerBindings } from '../src/cpu_profiler'; +import { ProfilingIntegration } from '../src/index'; + +function makeClientWithoutHooks(): [Sentry.NodeClient, Transport] { + const integration = new ProfilingIntegration(); + const transport = Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }); + const client = new Sentry.NodeClient({ + stackParser: Sentry.defaultStackParser, + tracesSampleRate: 1, + profilesSampleRate: 1, + debug: true, + environment: 'test-environment', + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [integration], + transport: _opts => transport, + }); + // eslint-disable-next-line deprecation/deprecation + client.setupIntegrations = () => { + integration.setupOnce( + cb => { + // @ts-expect-error __SENTRY__ is a private property + getMainCarrier().__SENTRY__.globalEventProcessors = [cb]; + }, + // eslint-disable-next-line deprecation/deprecation + () => Sentry.getCurrentHub(), + ); + }; + // @ts-expect-error override private + client.on = undefined; + return [client, transport]; +} + +function makeClientWithHooks(): [Sentry.NodeClient, Transport] { + const integration = new ProfilingIntegration(); + const client = new Sentry.NodeClient({ + stackParser: Sentry.defaultStackParser, + tracesSampleRate: 1, + profilesSampleRate: 1, + debug: true, + environment: 'test-environment', + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [integration], + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + }); + + // eslint-disable-next-line deprecation/deprecation + client.setupIntegrations = () => { + integration.setupOnce( + cb => { + // @ts-expect-error __SENTRY__ is a private property + getMainCarrier().__SENTRY__.globalEventProcessors = [cb]; + }, + // eslint-disable-next-line deprecation/deprecation + () => Sentry.getCurrentHub(), + ); + }; + + return [client, client.getTransport() as Transport]; +} + +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +describe('hubextensions', () => { + beforeEach(() => { + jest.useRealTimers(); + // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited + getMainCarrier().__SENTRY__ = {}; + GLOBAL_OBJ._sentryDebugIds = undefined as any; + }); + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + delete getMainCarrier().__SENTRY__; + }); + + it('pulls environment from sdk init', async () => { + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve()); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentHub().startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ environment: 'test-environment' }); + }); + + it('logger warns user if there are insufficient samples and discards the profile', async () => { + const logSpy = jest.spyOn(logger, 'log'); + + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); + + jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve()); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentHub().startTransaction({ name: 'profile_hub' }); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + expect(logSpy.mock?.calls[logSpy.mock.calls.length - 1]?.[0]).toBe( + '[Profiling] Discarding profile because it contains less than 2 samples', + ); + + expect((transport.send as any).mock.calls[0][0][1][0][0].type).toBe('transaction'); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transport.send).toHaveBeenCalledTimes(1); + }); + + it('logger warns user if traceId is invalid', async () => { + const logSpy = jest.spyOn(logger, 'log'); + + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + resources: [], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', + }; + }); + + jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve()); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentHub().startTransaction({ name: 'profile_hub', traceId: 'boop' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + expect(logSpy.mock?.calls?.[6]?.[0]).toBe('[Profiling] Invalid traceId: ' + 'boop' + ' on profiled event'); + }); + + describe('with hooks', () => { + it('calls profiler when transaction is started/stopped', async () => { + const [client, transport] = makeClientWithHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve()); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + }); + + it('sends profile in the same envelope as transaction', async () => { + const [client, transport] = makeClientWithHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve()); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + // One for profile, the other for transaction + expect(transportSpy).toHaveBeenCalledTimes(1); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); + }); + + it('does not crash if transaction has no profile context or it is invalid', async () => { + const [client] = makeClientWithHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // @ts-expect-error transaction is partial + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); + // @ts-expect-error transaction is partial + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); + client.emit( + 'beforeEnvelope', + // @ts-expect-error transaction is partial + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), + ); + client.emit( + 'beforeEnvelope', + // @ts-expect-error transaction is partial + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), + ); + + // Emit is sync, so we can just assert that we got here + expect(true).toBe(true); + }); + + it('if transaction was profiled, but profiler returned null', async () => { + const [client, transport] = makeClientWithHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); + // Emit is sync, so we can just assert that we got here + const transportSpy = jest.spyOn(transport, 'send').mockImplementation(() => { + // Do nothing so we don't send events to Sentry + return Promise.resolve(); + }); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + // Only transaction is sent + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); + }); + + it('emits preprocessEvent for profile', async () => { + const [client] = makeClientWithHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + const onPreprocessEvent = jest.fn(); + + client.on('preprocessEvent', onPreprocessEvent); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + expect(onPreprocessEvent.mock.calls[1][0]).toMatchObject({ + profile: { + samples: expect.arrayContaining([expect.anything()]), + stacks: expect.arrayContaining([expect.anything()]), + }, + }); + }); + }); + + describe('without hooks', () => { + it('calls profiler when transaction is started/stopped', async () => { + const [client] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect((stopProfilingSpy.mock.calls[startProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + }); + + it('sends profile in separate envelope', async () => { + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + const transportSpy = jest.spyOn(transport, 'send').mockImplementation(() => { + // Do nothing so we don't send events to Sentry + return Promise.resolve(); + }); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + // One for profile, the other for transaction + expect(transportSpy).toHaveBeenCalledTimes(2); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'profile' }); + }); + + it('respect max profile duration timeout', async () => { + // it seems that in node 19 globals (or least part of them) are a readonly object + // so when useFakeTimers is called it throws an error because it cannot override + // a readonly property of performance on global object. Use legacyFakeTimers for now + jest.useFakeTimers('legacy'); + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentHub().startTransaction({ name: 'timeout_transaction' }); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(30001); + + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + expect((stopProfilingSpy.mock.calls[startProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + }); + + it('does not crash if stop is called multiple times', async () => { + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.getCurrentHub().startTransaction({ name: 'txn' }); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it('enriches profile with debug_id', async () => { + GLOBAL_OBJ._sentryDebugIds = { + 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }; + + jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + resources: ['filename.js', 'filename2.js'], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', + }; + }); + + const [client, transport] = makeClientWithHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve()); + + // eslint-disable-next-line deprecation/deprecation + const transaction = hub.startTransaction({ name: 'profile_hub' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(1000); + + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'filename.js', + }, + { + type: 'sourcemap', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + code_file: 'filename2.js', + }, + ], + }, + }); + }); +}); diff --git a/packages/profiling-node/test/hubextensions.test.ts b/packages/profiling-node/test/hubextensions.test.ts new file mode 100644 index 000000000000..df90200c2a5d --- /dev/null +++ b/packages/profiling-node/test/hubextensions.test.ts @@ -0,0 +1,242 @@ +import type { + BaseTransportOptions, + ClientOptions, + Context, + Hub, + Transaction, + TransactionMetadata, +} from '@sentry/types'; + +import type { NodeClient } from '@sentry/node'; + +import { CpuProfilerBindings } from '../src/cpu_profiler'; +import { __PRIVATE__wrapStartTransactionWithProfiling } from '../src/hubextensions'; + +function makeTransactionMock(options = {}): Transaction { + return { + metadata: {}, + tags: {}, + sampled: true, + contexts: {}, + startChild: () => ({ finish: () => void 0 }), + finish() { + return; + }, + toContext: () => { + return {}; + }, + setContext(this: Transaction, key: string, context: Context) { + // @ts-expect-error - contexts is private + this.contexts[key] = context; + }, + setTag(this: Transaction, key: string, value: any) { + // eslint-disable-next-line deprecation/deprecation + this.tags[key] = value; + }, + setMetadata(this: Transaction, metadata: Partial) { + // eslint-disable-next-line deprecation/deprecation + this.metadata = { ...metadata } as TransactionMetadata; + }, + ...options, + } as unknown as Transaction; +} + +function makeHubMock({ + profilesSampleRate, + client, +}: { + profilesSampleRate: number | undefined; + client?: Partial; +}): Hub { + return { + getClient: jest.fn().mockImplementation(() => { + return { + getOptions: jest.fn().mockImplementation(() => { + return { + profilesSampleRate, + } as unknown as ClientOptions; + }), + ...(client ?? {}), + }; + }), + } as unknown as Hub; +} + +describe('hubextensions', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('skips profiling if profilesSampleRate is not set (undefined)', () => { + const hub = makeHubMock({ profilesSampleRate: undefined }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, {}); + transaction.finish(); + + expect(startTransaction).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect((transaction.metadata as any)?.profile).toBeUndefined(); + }); + it('skips profiling if profilesSampleRate is set to 0', () => { + const hub = makeHubMock({ profilesSampleRate: 0 }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, {}); + transaction.finish(); + + expect(startTransaction).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).not.toHaveBeenCalled(); + + expect((transaction.metadata as any)?.profile).toBeUndefined(); + }); + it('skips profiling when random > sampleRate', () => { + const hub = makeHubMock({ profilesSampleRate: 0.5 }); + jest.spyOn(global.Math, 'random').mockReturnValue(1); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, {}); + transaction.finish(); + + expect(startTransaction).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).not.toHaveBeenCalled(); + + expect((transaction.metadata as any)?.profile).toBeUndefined(); + }); + it('starts the profiler', () => { + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const hub = makeHubMock({ profilesSampleRate: 1 }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, {}); + transaction.finish(); + + expect(startTransaction).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + + expect((transaction.metadata as any)?.profile).toBeDefined(); + }); + + it('does not start the profiler if transaction is sampled', () => { + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const hub = makeHubMock({ profilesSampleRate: 1 }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock({ sampled: false })); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, {}); + transaction.finish(); + + expect(startTransaction).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + expect(stopProfilingSpy).not.toHaveBeenCalledTimes(1); + }); + + it('disabled if neither profilesSampler and profilesSampleRate are not set', () => { + const hub = makeHubMock({ profilesSampleRate: undefined }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const samplingContext = { beep: 'boop' }; + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, samplingContext); + transaction.finish(); + + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); + + it('does not call startProfiling if profilesSampler returns invalid rate', () => { + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const options = { profilesSampler: jest.fn().mockReturnValue(NaN) }; + const hub = makeHubMock({ + profilesSampleRate: undefined, + client: { + // @ts-expect-error partial client + getOptions: () => options, + }, + }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const samplingContext = { beep: 'boop' }; + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, samplingContext); + transaction.finish(); + + expect(options.profilesSampler).toHaveBeenCalled(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); + + it('does not call startProfiling if profilesSampleRate is invalid', () => { + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const options = { profilesSampler: jest.fn().mockReturnValue(NaN) }; + const hub = makeHubMock({ + profilesSampleRate: NaN, + client: { + // @ts-expect-error partial client + getOptions: () => options, + }, + }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const samplingContext = { beep: 'boop' }; + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, samplingContext); + transaction.finish(); + + expect(options.profilesSampler).toHaveBeenCalled(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); + + it('calls profilesSampler with sampling context', () => { + const options = { profilesSampler: jest.fn() }; + const hub = makeHubMock({ + profilesSampleRate: undefined, + client: { + // @ts-expect-error partial client + getOptions: () => options, + }, + }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const samplingContext = { beep: 'boop' }; + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, samplingContext); + transaction.finish(); + + expect(options.profilesSampler).toHaveBeenCalledWith({ + ...samplingContext, + transactionContext: transaction.toContext(), + }); + }); + + it('prioritizes profilesSampler outcome over profilesSampleRate', () => { + const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); + const options = { profilesSampler: jest.fn().mockReturnValue(1) }; + const hub = makeHubMock({ + profilesSampleRate: 0, + client: { + // @ts-expect-error partial client + getOptions: () => options, + }, + }); + const startTransaction = jest.fn().mockImplementation(() => makeTransactionMock()); + + const maybeStartTransactionWithProfiling = __PRIVATE__wrapStartTransactionWithProfiling(startTransaction); + const samplingContext = { beep: 'boop' }; + const transaction = maybeStartTransactionWithProfiling.call(hub, { name: '' }, samplingContext); + transaction.finish(); + + expect(startProfilingSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/profiling-node/test/index.test.ts b/packages/profiling-node/test/index.test.ts new file mode 100644 index 000000000000..ab6aaebfb86a --- /dev/null +++ b/packages/profiling-node/test/index.test.ts @@ -0,0 +1,166 @@ +import * as Sentry from '@sentry/node'; +import type { Transport } from '@sentry/types'; + +import { getMainCarrier } from '@sentry/core'; +import { ProfilingIntegration } from '../src/index'; +import type { Profile } from '../src/types'; + +interface MockTransport extends Transport { + events: any[]; +} + +function makeStaticTransport(): MockTransport { + return { + events: [] as any[], + send: function (...args: any[]) { + this.events.push(args); + return Promise.resolve(); + }, + flush: function () { + return Promise.resolve(true); + }, + }; +} + +function makeClientWithoutHooks(): [Sentry.NodeClient, MockTransport] { + const integration = new ProfilingIntegration(); + const transport = makeStaticTransport(); + const client = new Sentry.NodeClient({ + stackParser: Sentry.defaultStackParser, + tracesSampleRate: 1, + profilesSampleRate: 1, + debug: true, + environment: 'test-environment', + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [integration], + transport: () => transport, + }); + // eslint-disable-next-line deprecation/deprecation + client.setupIntegrations = () => { + integration.setupOnce( + cb => { + // @ts-expect-error __SENTRY__ is private + getMainCarrier().__SENTRY__.globalEventProcessors = [cb]; + }, + // eslint-disable-next-line deprecation/deprecation + () => Sentry.getCurrentHub(), + ); + }; + // @ts-expect-error override private property + client.on = undefined; + return [client, transport]; +} + +function findAllProfiles(transport: MockTransport): [any, Profile][] | null { + return transport?.events.filter(call => { + return call[0][1][0][0].type === 'profile'; + }); +} + +function findProfile(transport: MockTransport): Profile | null { + return ( + transport?.events.find(call => { + return call[0][1][0][0].type === 'profile'; + })?.[0][1][0][1] ?? null + ); +} + +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +describe('Sentry - Profiling', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited + getMainCarrier().__SENTRY__ = {}; + }); + afterEach(() => { + delete getMainCarrier().__SENTRY__; + }); + describe('without hooks', () => { + it('profiles a transaction', async () => { + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.startTransaction({ name: 'title' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(500); + expect(findProfile(transport)).not.toBe(null); + }); + + it('can profile overlapping transactions', async () => { + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // eslint-disable-next-line deprecation/deprecation + const t1 = Sentry.startTransaction({ name: 'outer' }); + // eslint-disable-next-line deprecation/deprecation + const t2 = Sentry.startTransaction({ name: 'inner' }); + await wait(500); + + // eslint-disable-next-line deprecation/deprecation + t2.finish(); + // eslint-disable-next-line deprecation/deprecation + t1.finish(); + + await Sentry.flush(500); + + expect(findAllProfiles(transport)?.[0]?.[0]?.[1]?.[0]?.[1].transaction.name).toBe('inner'); + expect(findAllProfiles(transport)?.[1]?.[0]?.[1]?.[0]?.[1].transaction.name).toBe('outer'); + expect(findAllProfiles(transport)).toHaveLength(2); + expect(findProfile(transport)).not.toBe(null); + }); + + it('does not discard overlapping transaction with same title', async () => { + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // eslint-disable-next-line deprecation/deprecation + const t1 = Sentry.startTransaction({ name: 'same-title' }); + // eslint-disable-next-line deprecation/deprecation + const t2 = Sentry.startTransaction({ name: 'same-title' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + t2.finish(); + // eslint-disable-next-line deprecation/deprecation + t1.finish(); + + await Sentry.flush(500); + expect(findAllProfiles(transport)).toHaveLength(2); + expect(findProfile(transport)).not.toBe(null); + }); + + it('does not crash if finish is called multiple times', async () => { + const [client, transport] = makeClientWithoutHooks(); + // eslint-disable-next-line deprecation/deprecation + const hub = Sentry.getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + hub.bindClient(client); + + // eslint-disable-next-line deprecation/deprecation + const transaction = Sentry.startTransaction({ name: 'title' }); + await wait(500); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + // eslint-disable-next-line deprecation/deprecation + transaction.finish(); + + await Sentry.flush(500); + expect(findAllProfiles(transport)).toHaveLength(1); + expect(findProfile(transport)).not.toBe(null); + }); + }); +}); diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts new file mode 100644 index 000000000000..8f336600fa84 --- /dev/null +++ b/packages/profiling-node/test/integration.test.ts @@ -0,0 +1,272 @@ +import { EventEmitter } from 'events'; + +import type { Event, Hub, Transport } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { ProfilingIntegration } from '../src/integration'; +import type { ProfiledEvent } from '../src/types'; + +function assertCleanProfile(event: ProfiledEvent | Event): void { + expect(event.sdkProcessingMetadata?.profile).toBeUndefined(); +} + +function makeProfiledEvent(): ProfiledEvent { + return { + type: 'transaction', + sdkProcessingMetadata: { + profile: { + profile_id: 'id', + profiler_logging_mode: 'lazy', + samples: [ + { + elapsed_since_start_ns: '0', + thread_id: '0', + stack_id: 0, + }, + { + elapsed_since_start_ns: '1', + thread_id: '0', + stack_id: 0, + }, + ], + measurements: {}, + frames: [], + stacks: [], + resources: [], + }, + }, + }; +} + +describe('ProfilingIntegration', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('has a name', () => { + expect(new ProfilingIntegration().name).toBe('ProfilingIntegration'); + }); + + it('stores a reference to getCurrentHub', () => { + const integration = new ProfilingIntegration(); + + const getCurrentHub = jest.fn().mockImplementation(() => { + return { + getClient: jest.fn(), + }; + }); + const addGlobalEventProcessor = () => void 0; + + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + expect(integration.getCurrentHub).toBe(getCurrentHub); + }); + + describe('without hooks', () => { + it('does not call transporter if null profile is received', () => { + const transport: Transport = { + send: jest.fn().mockImplementation(() => Promise.resolve()), + flush: jest.fn().mockImplementation(() => Promise.resolve()), + }; + const integration = new ProfilingIntegration(); + + const getCurrentHub = jest.fn((): Hub => { + return { + getClient: () => { + return { + getOptions: () => { + return { + _metadata: {}, + }; + }, + getDsn: () => { + return {}; + }, + getTransport: () => transport, + }; + }, + } as Hub; + }); + const addGlobalEventProcessor = () => void 0; + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + integration.handleGlobalEvent({ + type: 'transaction', + sdkProcessingMetadata: { + profile: null, + }, + }); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transport.send).not.toHaveBeenCalled(); + }); + + it('when Hub.getClient returns undefined', async () => { + const logSpy = jest.spyOn(logger, 'log'); + const integration = new ProfilingIntegration(); + + const getCurrentHub = jest.fn((): Hub => { + return { getClient: () => undefined } as Hub; + }); + const addGlobalEventProcessor = () => void 0; + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + assertCleanProfile(await integration.handleGlobalEvent(makeProfiledEvent())); + expect(logSpy).toHaveBeenCalledWith( + '[Profiling] getClient did not return a Client, removing profile from event and forwarding to next event processors.', + ); + }); + it('when getDsn returns undefined', async () => { + const logSpy = jest.spyOn(logger, 'log'); + const integration = new ProfilingIntegration(); + + const getCurrentHub = jest.fn((): Hub => { + return { + getClient: () => { + return { + getDsn: () => undefined, + }; + }, + } as Hub; + }); + const addGlobalEventProcessor = () => void 0; + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + assertCleanProfile(await integration.handleGlobalEvent(makeProfiledEvent())); + expect(logSpy).toHaveBeenCalledWith( + '[Profiling] getDsn did not return a Dsn, removing profile from event and forwarding to next event processors.', + ); + }); + it('when getTransport returns undefined', async () => { + const logSpy = jest.spyOn(logger, 'log'); + const integration = new ProfilingIntegration(); + + const getCurrentHub = jest.fn((): Hub => { + return { + getClient: () => { + return { + getDsn: () => { + return {}; + }, + getTransport: () => undefined, + }; + }, + } as Hub; + }); + const addGlobalEventProcessor = () => void 0; + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + assertCleanProfile(await integration.handleGlobalEvent(makeProfiledEvent())); + expect(logSpy).toHaveBeenCalledWith( + '[Profiling] getTransport did not return a Transport, removing profile from event and forwarding to next event processors.', + ); + }); + + it('sends profile to sentry', async () => { + const logSpy = jest.spyOn(logger, 'log'); + const transport: Transport = { + send: jest.fn().mockImplementation(() => Promise.resolve()), + flush: jest.fn().mockImplementation(() => Promise.resolve()), + }; + const integration = new ProfilingIntegration(); + + const getCurrentHub = jest.fn((): Hub => { + return { + getClient: () => { + return { + getOptions: () => { + return { + _metadata: {}, + }; + }, + getDsn: () => { + return {}; + }, + getTransport: () => transport, + }; + }, + } as Hub; + }); + const addGlobalEventProcessor = () => void 0; + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + assertCleanProfile(await integration.handleGlobalEvent(makeProfiledEvent())); + expect(logSpy.mock.calls?.[1]?.[0]).toBe('[Profiling] Preparing envelope and sending a profiling event'); + }); + }); + + describe('with SDK hooks', () => { + it('does not call transporter if null profile is received', () => { + const transport: Transport = { + send: jest.fn().mockImplementation(() => Promise.resolve()), + flush: jest.fn().mockImplementation(() => Promise.resolve()), + }; + const integration = new ProfilingIntegration(); + const emitter = new EventEmitter(); + + const getCurrentHub = jest.fn((): Hub => { + return { + getClient: () => { + return { + on: emitter.on.bind(emitter), + emit: emitter.emit.bind(emitter), + getOptions: () => { + return { + _metadata: {}, + }; + }, + getDsn: () => { + return {}; + }, + getTransport: () => transport, + } as any; + }, + } as Hub; + }); + + const addGlobalEventProcessor = () => void 0; + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transport.send).not.toHaveBeenCalled(); + }); + + it('binds to startTransaction, finishTransaction and beforeEnvelope', () => { + const transport: Transport = { + send: jest.fn().mockImplementation(() => Promise.resolve()), + flush: jest.fn().mockImplementation(() => Promise.resolve()), + }; + const integration = new ProfilingIntegration(); + const emitter = new EventEmitter(); + + const getCurrentHub = jest.fn((): Hub => { + return { + getClient: () => { + return { + on: emitter.on.bind(emitter), + emit: emitter.emit.bind(emitter), + getOptions: () => { + return { + _metadata: {}, + }; + }, + getDsn: () => { + return {}; + }, + getTransport: () => transport, + } as any; + }, + } as Hub; + }); + + const spy = jest.spyOn(emitter, 'on'); + + const addGlobalEventProcessor = jest.fn(); + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy.mock?.calls?.[0]?.[0]).toBe('startTransaction'); + expect(spy.mock?.calls?.[1]?.[0]).toBe('finishTransaction'); + expect(spy.mock?.calls?.[2]?.[0]).toBe('beforeEnvelope'); + + expect(addGlobalEventProcessor).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/profiling-node/test/utils.test.ts b/packages/profiling-node/test/utils.test.ts new file mode 100644 index 000000000000..640d0eace7f2 --- /dev/null +++ b/packages/profiling-node/test/utils.test.ts @@ -0,0 +1,361 @@ +import type { DsnComponents, Event, SdkMetadata } from '@sentry/types'; +import { addItemToEnvelope, createEnvelope, uuid4 } from '@sentry/utils'; + +import { + addProfilesToEnvelope, + findProfiledTransactionsFromEnvelope, + isValidProfile, + isValidSampleRate, +} from '../src/utils'; + +import type { Profile, ProfiledEvent } from '../src/types'; +import { + createProfilingEventEnvelope, + isProfiledTransactionEvent, + maybeRemoveProfileFromSdkMetadata, +} from '../src/utils'; + +function makeSdkMetadata(props: Partial): SdkMetadata { + return { + sdk: { + ...props, + }, + }; +} + +function makeDsn(props: Partial): DsnComponents { + return { + protocol: 'http', + projectId: '1', + host: 'localhost', + ...props, + }; +} + +function makeEvent( + props: Partial, + profile: NonNullable, +): ProfiledEvent { + return { ...props, sdkProcessingMetadata: { profile: profile } }; +} + +function makeProfile( + props: Partial, +): NonNullable { + return { + profile_id: '1', + profiler_logging_mode: 'lazy', + stacks: [], + samples: [ + { elapsed_since_start_ns: '0', thread_id: '0', stack_id: 0 }, + { elapsed_since_start_ns: '10', thread_id: '0', stack_id: 0 }, + ], + measurements: {}, + resources: [], + frames: [], + ...props, + }; +} + +describe('isProfiledTransactionEvent', () => { + it('profiled event', () => { + expect(isProfiledTransactionEvent({ sdkProcessingMetadata: { profile: {} } })).toBe(true); + }); + it('not profiled event', () => { + expect(isProfiledTransactionEvent({ sdkProcessingMetadata: { something: {} } })).toBe(false); + }); +}); + +describe('maybeRemoveProfileFromSdkMetadata', () => { + it('removes profile', () => { + expect(maybeRemoveProfileFromSdkMetadata({ sdkProcessingMetadata: { profile: {} } })).toEqual({ + sdkProcessingMetadata: {}, + }); + }); + + it('does nothing', () => { + expect(maybeRemoveProfileFromSdkMetadata({ sdkProcessingMetadata: { something: {} } })).toEqual({ + sdkProcessingMetadata: { something: {} }, + }); + }); +}); + +describe('createProfilingEventEnvelope', () => { + it('throws if profile_id is not set', () => { + const profile = makeProfile({}); + delete profile.profile_id; + + expect(() => + createProfilingEventEnvelope(makeEvent({ type: 'transaction' }, profile), makeDsn({}), makeSdkMetadata({})), + ).toThrow('Cannot construct profiling event envelope without a valid profile id. Got undefined instead.'); + }); + it('throws if profile is undefined', () => { + expect(() => + // @ts-expect-error mock profile as undefined + createProfilingEventEnvelope(makeEvent({ type: 'transaction' }, undefined), makeDsn({}), makeSdkMetadata({})), + ).toThrow('Cannot construct profiling event envelope without a valid profile. Got undefined instead.'); + expect(() => + // @ts-expect-error mock profile as null + createProfilingEventEnvelope(makeEvent({ type: 'transaction' }, null), makeDsn({}), makeSdkMetadata({})), + ).toThrow('Cannot construct profiling event envelope without a valid profile. Got null instead.'); + }); + + it('envelope header is of type: profile', () => { + const envelope = createProfilingEventEnvelope( + makeEvent( + { type: 'transaction' }, + makeProfile({ + samples: [ + { elapsed_since_start_ns: '0', thread_id: '0', stack_id: 0 }, + { elapsed_since_start_ns: '0', thread_id: '0', stack_id: 0 }, + ], + }), + ), + makeDsn({}), + makeSdkMetadata({ + name: 'sentry.javascript.node', + version: '1.2.3', + integrations: ['integration1', 'integration2'], + packages: [ + { name: 'package1', version: '1.2.3' }, + { name: 'package2', version: '4.5.6' }, + ], + }), + ); + expect(envelope?.[1][0]?.[0].type).toBe('profile'); + }); + + it('returns if samples.length <= 1', () => { + const envelope = createProfilingEventEnvelope( + makeEvent( + { type: 'transaction' }, + makeProfile({ + samples: [{ elapsed_since_start_ns: '0', thread_id: '0', stack_id: 0 }], + }), + ), + makeDsn({}), + makeSdkMetadata({ + name: 'sentry.javascript.node', + version: '1.2.3', + integrations: ['integration1', 'integration2'], + packages: [ + { name: 'package1', version: '1.2.3' }, + { name: 'package2', version: '4.5.6' }, + ], + }), + ); + expect(envelope).toBe(null); + }); + + it('enriches envelope with sdk metadata', () => { + const envelope = createProfilingEventEnvelope( + makeEvent({ type: 'transaction' }, makeProfile({})), + makeDsn({}), + makeSdkMetadata({ + name: 'sentry.javascript.node', + version: '1.2.3', + }), + ); + + expect(envelope && envelope[0]?.sdk?.name).toBe('sentry.javascript.node'); + expect(envelope && envelope[0]?.sdk?.version).toBe('1.2.3'); + }); + + it('handles undefined sdk metadata', () => { + const envelope = createProfilingEventEnvelope( + makeEvent({ type: 'transaction' }, makeProfile({})), + makeDsn({}), + undefined, + ); + + expect(envelope?.[0].sdk).toBe(undefined); + }); + + it('enriches envelope with dsn metadata', () => { + const envelope = createProfilingEventEnvelope( + makeEvent({ type: 'transaction' }, makeProfile({})), + makeDsn({ + host: 'sentry.io', + projectId: '123', + protocol: 'https', + path: 'path', + port: '9000', + publicKey: 'publicKey', + }), + makeSdkMetadata({}), + 'tunnel', + ); + + expect(envelope?.[0].dsn).toBe('https://publicKey@sentry.io:9000/path/123'); + }); + + it('enriches profile with device info', () => { + const envelope = createProfilingEventEnvelope( + makeEvent({ type: 'transaction' }, makeProfile({})), + makeDsn({}), + makeSdkMetadata({}), + ); + const profile = envelope?.[1][0]?.[1] as unknown as Profile; + + expect(typeof profile.device.manufacturer).toBe('string'); + expect(typeof profile.device.model).toBe('string'); + expect(typeof profile.os.name).toBe('string'); + expect(typeof profile.os.version).toBe('string'); + + expect(profile.device.manufacturer.length).toBeGreaterThan(0); + expect(profile.device.model.length).toBeGreaterThan(0); + expect(profile.os.name.length).toBeGreaterThan(0); + expect(profile.os.version.length).toBeGreaterThan(0); + }); + + it('throws if event.type is not a transaction', () => { + expect(() => + createProfilingEventEnvelope( + makeEvent( + // @ts-expect-error force invalid value + { type: 'error' }, + // @ts-expect-error mock tid as undefined + makeProfile({ samples: [{ stack_id: 0, thread_id: undefined, elapsed_since_start_ns: '0' }] }), + ), + makeDsn({}), + makeSdkMetadata({}), + ), + ).toThrow('Profiling events may only be attached to transactions, this should never occur.'); + }); + + it('inherits transaction properties', () => { + const start = new Date(2022, 8, 1, 12, 0, 0); + const end = new Date(2022, 8, 1, 12, 0, 10); + + const envelope = createProfilingEventEnvelope( + makeEvent( + { + event_id: uuid4(), + type: 'transaction', + transaction: 'transaction-name', + start_timestamp: start.getTime() / 1000, + timestamp: end.getTime() / 1000, + contexts: { + trace: { + span_id: 'span_id', + trace_id: 'trace_id', + }, + }, + }, + makeProfile({ + samples: [ + // @ts-expect-error mock tid as undefined + { stack_id: 0, thread_id: undefined, elapsed_since_start_ns: '0' }, + // @ts-expect-error mock tid as undefined + { stack_id: 0, thread_id: undefined, elapsed_since_start_ns: '0' }, + ], + }), + ), + makeDsn({}), + makeSdkMetadata({}), + ); + + const profile = envelope?.[1][0]?.[1] as unknown as Profile; + + expect(profile.transaction.name).toBe('transaction-name'); + expect(typeof profile.transaction.id).toBe('string'); + expect(profile.transaction.id?.length).toBe(32); + expect(profile.transaction.trace_id).toBe('trace_id'); + }); +}); + +describe('isValidSampleRate', () => { + it.each([ + [0, true], + [0.1, true], + [1, true], + [true, true], + [false, true], + // invalid values + [1.1, false], + [-0.1, false], + [NaN, false], + [Infinity, false], + [null, false], + [undefined, false], + ['', false], + [' ', false], + [{}, false], + [[], false], + [() => null, false], + ])('value %s is %s', (input, expected) => { + expect(isValidSampleRate(input)).toBe(expected); + }); +}); + +describe('isValidProfile', () => { + it('is not valid if samples <= 1', () => { + expect(isValidProfile(makeProfile({ samples: [] }))).toBe(false); + }); + + it('is not valid if it does not have a profile_id', () => { + expect(isValidProfile(makeProfile({ samples: [], profile_id: undefined } as any))).toBe(false); + }); +}); + +describe('addProfilesToEnvelope', () => { + it('adds profile', () => { + const profile = makeProfile({}); + const envelope = createEnvelope({}); + + // @ts-expect-error profile is untyped + addProfilesToEnvelope(envelope, [profile]); + + // @ts-expect-error profile is untyped + const addedBySdk = addItemToEnvelope(createEnvelope({}), [{ type: 'profile' }, profile]); + + expect(envelope?.[1][0]?.[0]).toEqual({ type: 'profile' }); + expect(envelope?.[1][0]?.[1]).toEqual(profile); + + expect(JSON.stringify(addedBySdk)).toEqual(JSON.stringify(envelope)); + }); +}); + +describe('findProfiledTransactionsFromEnvelope', () => { + it('returns transactions with profile context', () => { + const txnWithProfile: Event = { + event_id: uuid4(), + type: 'transaction', + contexts: { + profile: { + profile_id: uuid4(), + }, + }, + }; + + const envelope = addItemToEnvelope(createEnvelope({}), [{ type: 'transaction' }, txnWithProfile]); + expect(findProfiledTransactionsFromEnvelope(envelope)[0]).toBe(txnWithProfile); + }); + + it('skips if transaction event is not profiled', () => { + const txnWithProfile: Event = { + event_id: uuid4(), + type: 'transaction', + contexts: {}, + }; + + const envelope = addItemToEnvelope(createEnvelope({}), [{ type: 'transaction' }, txnWithProfile]); + expect(findProfiledTransactionsFromEnvelope(envelope)[0]).toBe(undefined); + }); + + it('skips if event is not a transaction', () => { + const nonTransactionEvent: Event = { + event_id: uuid4(), + type: 'replay_event', + contexts: { + profile: { + profile_id: uuid4(), + }, + }, + }; + + // @ts-expect-error replay event is partial + const envelope = addItemToEnvelope(createEnvelope({}), [{ type: 'replay_event' }, nonTransactionEvent]); + expect(findProfiledTransactionsFromEnvelope(envelope)[0]).toBe(undefined); + }); +}); diff --git a/packages/profiling-node/tsconfig.json b/packages/profiling-node/tsconfig.json new file mode 100644 index 000000000000..0c4404d8d70c --- /dev/null +++ b/packages/profiling-node/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "lib": ["es6"], + "outDir": "lib", + "types": ["node"] + }, + "include": ["src/**/*"] +} + diff --git a/packages/profiling-node/tsconfig.test.json b/packages/profiling-node/tsconfig.test.json new file mode 100644 index 000000000000..52333183eb70 --- /dev/null +++ b/packages/profiling-node/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "src/**/*.d.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "jest"] + + // other package-specific, test-specific options + } +} diff --git a/packages/profiling-node/tsconfig.types.json b/packages/profiling-node/tsconfig.types.json new file mode 100644 index 000000000000..d613534a1674 --- /dev/null +++ b/packages/profiling-node/tsconfig.types.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "lib/types", + "types": ["node"] + }, + "files": ["src/index.ts"] +} diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index 567224854baa..9c9312e76571 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -19,6 +19,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [ '@sentry/react', '@sentry/angular', '@sentry/svelte', + '@sentry/profiling-node', '@sentry/replay', '@sentry-internal/replay-canvas', '@sentry-internal/feedback', diff --git a/yarn.lock b/yarn.lock index 8b1623ec39bb..de4af962b216 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5440,6 +5440,15 @@ fflate "^0.4.4" mitt "^3.0.0" +"@sentry-internal/tracing@7.93.0": + version "7.93.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.93.0.tgz#8cee8b610695d828af75edd2929b64b7caf0385d" + integrity sha512-DjuhmQNywPp+8fxC9dvhGrqgsUb6wI/HQp25lS2Re7VxL1swCasvpkg8EOYP4iBniVQ86QK0uITkOIRc5tdY1w== + dependencies: + "@sentry/core" "7.93.0" + "@sentry/types" "7.93.0" + "@sentry/utils" "7.93.0" + "@sentry/bundler-plugin-core@0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-0.6.1.tgz#6c6a2ff3cdc98cd0ff1c30c59408cee9f067adf2" @@ -5534,6 +5543,46 @@ "@sentry/cli-win32-i686" "2.26.0" "@sentry/cli-win32-x64" "2.26.0" +"@sentry/core@7.93.0": + version "7.93.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.93.0.tgz#50a14bf305130dfef51810e4c97fcba4972a57ef" + integrity sha512-vZQSUiDn73n+yu2fEcH+Wpm4GbRmtxmnXnYCPgM6IjnXqkVm3awWAkzrheADblx3kmxrRiOlTXYHw9NTWs56fg== + dependencies: + "@sentry/types" "7.93.0" + "@sentry/utils" "7.93.0" + +"@sentry/hub@7.93.0": + version "7.93.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.93.0.tgz#3db26f74dc1269650fa9ec553f0837af5caa0d30" + integrity sha512-gfPyT3DFGYYM5d+CHiS0YJHkIJHa8MKSfl32RkCMA5KQr9RF0H+GR5LZCinQOWq43fpsncSLyuoMfBjgbMLN+w== + dependencies: + "@sentry/core" "7.93.0" + "@sentry/types" "7.93.0" + "@sentry/utils" "7.93.0" + +"@sentry/node@7.93.0": + version "7.93.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.93.0.tgz#7786d05d1e3e984207a866b07df1bf891355892e" + integrity sha512-nUXPCZQm5Y9Ipv7iWXLNp5dbuyi1VvbJ3RtlwD7utgsNkRYB4ixtKE9w2QU8DZZAjaEF6w2X94OkYH6C932FWw== + dependencies: + "@sentry-internal/tracing" "7.93.0" + "@sentry/core" "7.93.0" + "@sentry/types" "7.93.0" + "@sentry/utils" "7.93.0" + https-proxy-agent "^5.0.0" + +"@sentry/types@7.93.0": + version "7.93.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.93.0.tgz#d76d26259b40cd0688e1d634462fbff31476c1ec" + integrity sha512-UnzUccNakhFRA/esWBWP+0v7cjNg+RilFBQC03Mv9OEMaZaS29zSbcOGtRzuFOXXLBdbr44BWADqpz3VW0XaNw== + +"@sentry/utils@7.93.0": + version "7.93.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.93.0.tgz#36225038661fe977baf01e4695ef84794d591e45" + integrity sha512-Iovj7tUnbgSkh/WrAaMrd5UuYjW7AzyzZlFDIUrwidsyIdUficjCG2OIxYzh76H6nYIx9SxewW0R54Q6XoB4uA== + dependencies: + "@sentry/types" "7.93.0" + "@sentry/vite-plugin@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-0.6.1.tgz#31eb744e8d87b1528eed8d41433647727a62e7c0" @@ -6531,6 +6580,11 @@ dependencies: "@types/unist" "^2" +"@types/node-abi@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/node-abi/-/node-abi-3.0.3.tgz#a8334d75fe45ccd4cdb2a6c1ae82540a7a76828c" + integrity sha512-5oos6sivyXcDEuVC5oX3+wLwfgrGZu4NIOn826PGAjPCHsqp2zSPTGU7H1Tv+GZBOiDUY3nBXY1MdaofSEt4fw== + "@types/node-fetch@^2.6.0": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" @@ -6544,6 +6598,11 @@ 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@16.18.70": + version "16.18.70" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.70.tgz#d4c819be1e9f8b69a794d6f2fd929d9ff76f6d4b" + integrity sha512-8eIk20G5VVVQNZNouHjLA2b8utE2NvGybLjMaF4lyhA9uhGwnmXF8o+icdXKGSQSNANJewXva/sFUoZLwAaYAg== + "@types/node@20.8.2": version "20.8.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" @@ -11226,6 +11285,15 @@ cjs-module-lexer@^1.2.2: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== +clang-format@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.8.0.tgz#7779df1c5ce1bc8aac1b0b02b4e479191ef21d46" + integrity sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw== + dependencies: + async "^3.2.3" + glob "^7.0.0" + resolve "^1.1.6" + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -12147,6 +12215,12 @@ cron@^3.1.6: dependencies: "@types/luxon" "~3.3.0" luxon "~3.4.0" +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" @@ -12159,7 +12233,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -15308,6 +15382,11 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + express@^4.10.7, express@^4.16.4, express@^4.17.1, express@^4.17.3, express@^4.18.1: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -22887,6 +22966,13 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-abi@^3.52.0: + version "3.54.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.54.0.tgz#f6386f7548817acac6434c6cba02999c9aebcc69" + integrity sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA== + dependencies: + semver "^7.3.5" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -22986,6 +23072,23 @@ node-gyp@^9.0.0: tar "^6.1.2" which "^2.0.2" +node-gyp@^9.4.1: + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-html-parser@1.4.9: version "1.4.9" resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c" @@ -30793,7 +30896,7 @@ typescript@4.3.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== -typescript@4.9.5: +typescript@4.9.5, typescript@^4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== From 0588a3f31e9646935bc73ecb837694ab487d426a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Feb 2024 08:12:31 +0100 Subject: [PATCH 34/68] feat(node-experimental): Update opentelemetry packages (#10456) Before we port these to node, eventually, let's get everything up to date. --- .../apollo-graphql/test.ts | 6 +- packages/node-experimental/package.json | 38 +- .../test/spanprocessor.test.ts | 4 +- yarn.lock | 359 +++++++++--------- 4 files changed, 203 insertions(+), 204 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts index 96018c12ebeb..230c4cd4dac3 100644 --- a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts @@ -26,7 +26,7 @@ conditionalTest({ min: 14 })('GraphQL/Apollo Tests', () => { 'otel.kind': 'INTERNAL', 'sentry.origin': 'manual', }, - description: 'graphql.resolve', + description: 'graphql.resolve hello', status: 'ok', origin: 'manual', }), @@ -44,9 +44,7 @@ conditionalTest({ min: 14 })('GraphQL/Apollo Tests', () => { data: { 'graphql.operation.name': 'Mutation', 'graphql.operation.type': 'mutation', - 'graphql.source': `mutation Mutation($email: String) { - login(email: $email) -}`, + 'graphql.source': 'mutation Mutation($email: String) {\n login(email: $email)\n}', 'otel.kind': 'INTERNAL', 'sentry.origin': 'auto.graphql.otel.graphql', }, diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 264bb55eb65b..3822da240dfb 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -29,25 +29,25 @@ "access": "public" }, "dependencies": { - "@opentelemetry/api": "~1.6.0", - "@opentelemetry/context-async-hooks": "~1.17.1", - "@opentelemetry/core": "~1.17.1", - "@opentelemetry/instrumentation": "0.44.0", - "@opentelemetry/instrumentation-express": "0.33.2", - "@opentelemetry/instrumentation-fastify": "0.32.3", - "@opentelemetry/instrumentation-graphql": "0.35.2", - "@opentelemetry/instrumentation-hapi": "0.33.1", - "@opentelemetry/instrumentation-http": "0.44.0", - "@opentelemetry/instrumentation-mongodb": "0.37.1", - "@opentelemetry/instrumentation-mongoose": "0.33.2", - "@opentelemetry/instrumentation-mysql": "0.34.2", - "@opentelemetry/instrumentation-mysql2": "0.34.2", - "@opentelemetry/instrumentation-nestjs-core": "0.33.2", - "@opentelemetry/instrumentation-pg": "0.36.2", - "@opentelemetry/resources": "~1.17.1", - "@opentelemetry/sdk-trace-base": "~1.17.1", - "@opentelemetry/semantic-conventions": "~1.17.1", - "@prisma/instrumentation": "5.4.2", + "@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-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": "7.99.0", "@sentry/node": "7.99.0", "@sentry/opentelemetry": "7.99.0", diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 0bd7d852f7a5..940f5c38bdab 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -301,7 +301,7 @@ describe('SentrySpanProcessor', () => { 'service.name': 'test-service', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.17.1', + 'telemetry.sdk.version': '1.21.0', }, }, }); @@ -326,7 +326,7 @@ describe('SentrySpanProcessor', () => { 'service.name': 'test-service', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.17.1', + 'telemetry.sdk.version': '1.21.0', }, }, }); diff --git a/yarn.lock b/yarn.lock index de4af962b216..1acf15085acd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4693,10 +4693,10 @@ dependencies: "@opentelemetry/context-base" "^0.14.0" -"@opentelemetry/api@1.6.0", "@opentelemetry/api@^1.6.0", "@opentelemetry/api@~1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" - integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== +"@opentelemetry/api@1.7.0", "@opentelemetry/api@^1.6.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40" + integrity sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw== "@opentelemetry/api@^0.12.0": version "0.12.0" @@ -4705,10 +4705,10 @@ dependencies: "@opentelemetry/context-base" "^0.12.0" -"@opentelemetry/context-async-hooks@1.17.1", "@opentelemetry/context-async-hooks@^1.17.1", "@opentelemetry/context-async-hooks@~1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.17.1.tgz#4eba80bd66f8cd367e9ba94b5fec5f5acf5d7b25" - integrity sha512-up5I+RiQEkGrVEHtbAtmRgS+ZOnFh3shaDNHqZPBlGy+O92auL6yMmjzYpSKmJOGWowvs3fhVHePa8Exb5iHUg== +"@opentelemetry/context-async-hooks@1.21.0", "@opentelemetry/context-async-hooks@^1.17.1": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.21.0.tgz#a56fa461e7786605bcbde2ff66f21b2392afacda" + integrity sha512-t0iulGPiMjG/NrSjinPQoIf8ST/o9V0dGOJthfrFporJlNdlKIQPfC7lkrV+5s2dyBThfmSbJlp/4hO1eOcDXA== "@opentelemetry/context-base@^0.12.0": version "0.12.0" @@ -4720,19 +4720,19 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== -"@opentelemetry/core@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.0.tgz#6a72425f5f953dc68b4c7c66d947c018173d30d2" - integrity sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg== +"@opentelemetry/core@1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.20.0.tgz#ab1a8204ed10cc11e17bb61db658da0f3686d4ac" + integrity sha512-lSRvk5AIdD6CtgYJcJXh0wGibQ3S/8bC2qbqKs9wK8e0K1tsWV6YkGFOqVc+jIRlCbZoIBeZzDe5UI+vb94uvg== dependencies: - "@opentelemetry/semantic-conventions" "1.17.0" + "@opentelemetry/semantic-conventions" "1.20.0" -"@opentelemetry/core@1.17.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.17.1", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@~1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.1.tgz#10c5e09c63aeb1836b34d80baf7113760fb19d96" - integrity sha512-I6LrZvl1FF97FQXPR0iieWQmKnGxYtMbWA1GrAXnLUR+B1Hn2m8KqQNEIlZAucyv00GBgpWkpllmULmZfG8P3g== +"@opentelemetry/core@1.21.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.17.1", "@opentelemetry/core@^1.8.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.21.0.tgz#8c16faf16edf861b073c03c9d45977b3f4003ee1" + integrity sha512-KP+OIweb3wYoP7qTYL/j5IpOlu52uxBv5M4+QhSmmUfLyTgu1OIS71msK3chFo1D6Y61BIH3wMiMYRCxJCQctA== dependencies: - "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/semantic-conventions" "1.21.0" "@opentelemetry/core@^0.12.0": version "0.12.0" @@ -4743,123 +4743,132 @@ "@opentelemetry/context-base" "^0.12.0" semver "^7.1.3" -"@opentelemetry/instrumentation-express@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.33.2.tgz#e5bd14be5814e24b257cd093220d32d5e9261c5a" - integrity sha512-FR05iNosZL42haYang6vpmcuLfXLngJs/0gAgqXk8vwqGGwilOFak1PjoRdO4PAoso0FI+3zhV3Tz7jyDOmSyA== +"@opentelemetry/instrumentation-express@0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.35.0.tgz#4391c46f4ce00d840633fd79391028c38eca01bc" + integrity sha512-ZmSB4WMd88sSecOL7DlghzdBl56/8ymb02n+xEJ/6zUgONuw/1uoTh1TAaNPKfEWdNLoLKXQm+Gd2zBrUVOX0w== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" - "@types/express" "4.17.18" -"@opentelemetry/instrumentation-fastify@0.32.3": - version "0.32.3" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.32.3.tgz#2c0640c986018d1a41dfff3d9c3bfe3b5b1cf62d" - integrity sha512-vRFVoEJXcu6nNpJ61H5syDb84PirOd4b3u8yl8Bcorrr6firGYBQH4pEIVB4PkQWlmi3sLOifqS3VAO2VRloEQ== +"@opentelemetry/instrumentation-fastify@0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.33.0.tgz#4f4013b2677c94d7f8f34e0aeab77bca16524d8e" + integrity sha512-sl3q9Mt+yM6GlZJKhfLUIRrVEYqfmI0hqYLha5OFG5rLrgnZCCZVy8ra4+Pa40ecH1409cvwwBPf7k9AHEQBTw== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-graphql@0.35.2": - version "0.35.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.35.2.tgz#67b0c173cff1191cfa66aa26f67c6752c365edf2" - integrity sha512-lJv7BbHFK0ExwogdQMtVHfnWhCBMDQEz8KYvhShXfRPiSStU5aVwa3TmT0O00KiJFpATSKJNZMv1iZNHbF6z1g== +"@opentelemetry/instrumentation-graphql@0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.37.0.tgz#0bafda083065074dacce9bd4b9d0f3183379d3ca" + integrity sha512-WL5Qn1aRudJDxVN0Ao73/yzXBGBJAH1Fd2tteuGXku/Qw9hYQ936CgoO66GWmSiq2lyjsojAk1t5f+HF9j3NXw== dependencies: - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" -"@opentelemetry/instrumentation-hapi@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.33.1.tgz#9327f15a0d075153f61d338400b3db618dd3902e" - integrity sha512-8gwPrIgppbj/prCTK31mGmcBvYESE5J2El6badbCvcUHg6ZSA/i8zo80NrJ6812imtD06Dvm6kfnK5UzlC+smQ== +"@opentelemetry/instrumentation-hapi@0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.34.0.tgz#edab0a9175ca141cd289d28c7d1677a2da80d993" + integrity sha512-qUENVxwCYbRbJ8HBY54ZL1Z9q1guCEurW6tCFFJJKQFu/MKEw7GSFImy5DR2Mp8b5ggZO36lYFcx0QUxfy4GJg== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@types/hapi__hapi" "20.0.13" -"@opentelemetry/instrumentation-http@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.44.0.tgz#5a3e4b91073f737f054fe42ef591c39c5b3e6394" - integrity sha512-Nlvj3Y2n9q6uIcQq9f33HbcB4Dr62erSwYA37+vkorYnzI2j9PhxKitocRTZnbYsrymYmQJW9mdq/IAfbtVnNg== +"@opentelemetry/instrumentation-http@0.48.0": + version "0.48.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.48.0.tgz#88266dfcd2dddb45f755a0f1fc882472e6e30a87" + integrity sha512-uXqOsLhW9WC3ZlGm6+PSX0xjSDTCfy4CMjfYj6TPWusOO8dtdx040trOriF24y+sZmS3M+5UQc6/3/ZxBJh4Mw== dependencies: - "@opentelemetry/core" "1.17.1" - "@opentelemetry/instrumentation" "0.44.0" - "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/core" "1.21.0" + "@opentelemetry/instrumentation" "0.48.0" + "@opentelemetry/semantic-conventions" "1.21.0" semver "^7.5.2" -"@opentelemetry/instrumentation-mongodb@0.37.1": - version "0.37.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.37.1.tgz#5957565a74a4fe39fb72ab29f3b72a20223ef3df" - integrity sha512-UE+5B/MDfB5MUlJfjj8uo/fMnJPpqeUesJZ/loAWuCLCTDDyEJM7wnAvtH+2c4QoukkkIT1lDe5q9aiXwLEr5g== +"@opentelemetry/instrumentation-mongodb@0.39.0": + version "0.39.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.39.0.tgz#31bc92c137b578219bfaf4d15c7f247bc8d3b2c6" + integrity sha512-m9dMj39pcCshzlfCEn2lGrlNo7eV5fb9pGBnPyl/Am9Crh7Or8vOqvByCNd26Dgf5J978zTdLGF+6tM8j1WOew== dependencies: - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/sdk-metrics" "^1.9.1" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-mongoose@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.33.2.tgz#99f235df66009e0b73953a58f3f6b9f28e6a31b1" - integrity sha512-JXhhn8vkGKbev6aBPkQ6dL5rDImQfucrub8mU7dknPPpCL850fSQ2qt2qLvyDXfawF5my6KWW0fkKJCeRA+ECw== +"@opentelemetry/instrumentation-mongoose@0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.35.0.tgz#b101dea4a47a6ed7b5e760208917ebbb2597e53c" + integrity sha512-gReBMWD2Oa/wBGRWyg6B2dbPHhgkpOqDio31gE3DbC4JaqCsMByyeix75rZSzZ71RQmVh3d4jRLsqUtNVBzcyg== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-mysql2@0.34.2": - version "0.34.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.34.2.tgz#f59f03c3135a8b50bad9cb3d5b55403008a8d0ba" - integrity sha512-Ac/KAHHtTz087P7I6JapBs+ofNOM+RPTDGwSe1ddnTj0xTAO0F6ITmRC1firnMdzDidI/wI+vmgnWclCB81xKQ== +"@opentelemetry/instrumentation-mysql2@0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.35.0.tgz#aea66385ad4ae8a19896be718e03849d34bfdd53" + integrity sha512-DI9NXYJBbQ72rjz1KCKerQFQE+Z4xRDoyYek6JpITv5BlhPboA8zKkltxyQLL06Y2RTFYslw1gvg+x9CWlRzJw== dependencies: - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/sql-common" "^0.40.0" -"@opentelemetry/instrumentation-mysql@0.34.2": - version "0.34.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.34.2.tgz#3372dc11010dce2f357a89a1e3f32359c4d34079" - integrity sha512-3OEhW1CB7b93PHIbQ5t8Aoj/dCqNWQBDBbyUXGy2zFbhEcJBVcLeBpy3w8VEjzNTfRC6cVwASuHRP0aLBIPNjQ== +"@opentelemetry/instrumentation-mysql@0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.35.0.tgz#d344dbd831b0d49c395f04ea419b352d2701e908" + integrity sha512-QKRHd3aFA2vKOPzIZ9Q3UIxYeNPweB62HGlX2l3shOKrUhrtTg2/BzaKpHQBy2f2nO2mxTF/mOFeVEDeANnhig== dependencies: - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@types/mysql" "2.15.22" -"@opentelemetry/instrumentation-nestjs-core@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.33.2.tgz#fb87031097a96c761db0823c2eff8deba452abbf" - integrity sha512-jrX/355K+myc5V/EQFouqQzBfy5qj+SyVMHIKqVymOx/zWFCvz1p9ChNiPOKzl2il3o/P/aOqBUN/qnRaGowlw== +"@opentelemetry/instrumentation-nestjs-core@0.34.0": + version "0.34.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.34.0.tgz#4bbbbf08d62fc78ca319f0c966a054e718f9da91" + integrity sha512-HvbcCVAMZEIFrJ0Si9AfjxOr14KcH5h/lq5zLQ8AjZJpW0WaeO/ox5UgFi3J73Br91WbZHRgbXxMeodNycJJuA== dependencies: - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-pg@0.36.2": - version "0.36.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.36.2.tgz#45947d19bbafabf5b350a76350ef4523deac13a5" - integrity sha512-KUjI8OGi7kicml2Sd/PR/M8otZoZEdPArMfhznS6OQKit+RxFo0p5x6RVeka/cLQlmoc3eeGBizDeZetssbHgw== +"@opentelemetry/instrumentation-pg@0.38.0": + version "0.38.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.38.0.tgz#19d49cc301ab63124a0482f21f64be3fbb81321c" + integrity sha512-Q7V/OJ1OZwaWYNOP/E9S6sfS03Z+PNU1SAjdAoXTj5j4u4iJSMSieLRWXFaHwsbefIOMkYvA00EBKF9IgbgbLA== dependencies: - "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.44.0" + "@opentelemetry/instrumentation" "^0.48.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/sql-common" "^0.40.0" "@types/pg" "8.6.1" "@types/pg-pool" "2.0.4" -"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" - integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== +"@opentelemetry/instrumentation@0.47.0": + version "0.47.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.47.0.tgz#1eaa26f2dd5a6ce8cadde9f86bd70f1e47df3d47" + integrity sha512-ZFhphFbowWwMahskn6BBJgMm8Z+TUx98IM+KpLIX3pwCK/zzgbCgwsJXRnjF9edDkc5jEhA7cEz/mP0CxfQkLA== dependencies: "@types/shimmer" "^1.0.2" - import-in-the-middle "1.4.2" + import-in-the-middle "^1.7.2" require-in-the-middle "^7.1.1" semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@0.44.0", "@opentelemetry/instrumentation@^0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.44.0.tgz#194f16fc96671575b6bd73d3fadffb5aa4497e67" - integrity sha512-B6OxJTRRCceAhhnPDBshyQO7K07/ltX3quOLu0icEvPK9QZ7r9P1y0RQX8O5DxB4vTv4URRkxkg+aFU/plNtQw== +"@opentelemetry/instrumentation@0.48.0", "@opentelemetry/instrumentation@^0.48.0": + version "0.48.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.48.0.tgz#a6dee936e973f1270c464657a55bb570807194aa" + integrity sha512-sjtZQB5PStIdCw5ovVTDGwnmQC+GGYArJNgIcydrDSqUTdYBnMrN9P4pwQZgS3vTGIp+TU1L8vMXGe51NVmIKQ== + dependencies: + "@types/shimmer" "^1.0.2" + import-in-the-middle "1.7.1" + require-in-the-middle "^7.1.1" + semver "^7.5.2" + shimmer "^1.2.1" + +"@opentelemetry/instrumentation@^0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" + integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== dependencies: "@types/shimmer" "^1.0.2" import-in-the-middle "1.4.2" @@ -4867,35 +4876,35 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/propagator-b3@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.17.1.tgz#65dbddf3763db82632ddd7ad1735e597ab7b2dc4" - integrity sha512-XEbXYb81AM3ayJLlbJqITPIgKBQCuby45ZHiB9mchnmQOffh6ZJOmXONdtZAV7TWzmzwvAd28vGSUk57Aw/5ZA== +"@opentelemetry/propagator-b3@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.21.0.tgz#72fadc4a07afb2c83f0830b8a06071e0361eacb2" + integrity sha512-3ZTobj2VDIOzLsIvvYCdpw6tunxUVElPxDvog9lS49YX4hohHeD84A8u9Ns/6UYUcaN5GSoEf891lzhcBFiOLA== dependencies: - "@opentelemetry/core" "1.17.1" + "@opentelemetry/core" "1.21.0" -"@opentelemetry/propagator-jaeger@1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.17.1.tgz#31cc43045a059d1ed3651b9f21d0fd6db817b02f" - integrity sha512-p+P4lf2pbqd3YMfZO15QCGsDwR2m1ke2q5+dq6YBLa/q0qiC2eq4cD/qhYBBed5/X4PtdamaVGHGsp+u3GXHDA== +"@opentelemetry/propagator-jaeger@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.21.0.tgz#bfc1fa3a050496ec67a253040dfdec4d16339225" + integrity sha512-8TQSwXjBmaDx7JkxRD7hdmBmRK2RGRgzHX1ArJfJhIc5trzlVweyorzqQrXOvqVEdEg+zxUMHkL5qbGH/HDTPA== dependencies: - "@opentelemetry/core" "1.17.1" + "@opentelemetry/core" "1.21.0" -"@opentelemetry/resources@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.0.tgz#ee29144cfd7d194c69698c8153dbadec7fe6819f" - integrity sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw== +"@opentelemetry/resources@1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.20.0.tgz#7165c39837e6e41b695f0088e40d15a5793f1469" + integrity sha512-nOpV0vGegSq+9ze2cEDvO3BMA5pGBhmhKZiAlj+xQZjiEjPmJtdHIuBLRvptu2ahcbFJw85gIB9BYHZOvZK1JQ== dependencies: - "@opentelemetry/core" "1.17.0" - "@opentelemetry/semantic-conventions" "1.17.0" + "@opentelemetry/core" "1.20.0" + "@opentelemetry/semantic-conventions" "1.20.0" -"@opentelemetry/resources@1.17.1", "@opentelemetry/resources@~1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.1.tgz#932f70f58c0e03fb1d38f0cba12672fd70804d99" - integrity sha512-M2e5emqg5I7qRKqlzKx0ROkcPyF8PbcSaWEdsm72od9txP7Z/Pl8PDYOyu80xWvbHAWk5mDxOF6v3vNdifzclA== +"@opentelemetry/resources@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.21.0.tgz#e773e918cc8ca26493a987dfbfc6b8a315a2ab45" + integrity sha512-1Z86FUxPKL6zWVy2LdhueEGl9AHDJcx+bvHStxomruz6Whd02mE3lNUMjVJ+FGRoktx/xYQcxccYb03DiUP6Yw== dependencies: - "@opentelemetry/core" "1.17.1" - "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/core" "1.21.0" + "@opentelemetry/semantic-conventions" "1.21.0" "@opentelemetry/resources@^0.12.0": version "0.12.0" @@ -4906,53 +4915,53 @@ "@opentelemetry/core" "^0.12.0" "@opentelemetry/sdk-metrics@^1.9.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.17.1.tgz#9c4d13d845bcc82be8684050d9db7cce10f61580" - integrity sha512-eHdpsMCKhKhwznxvEfls8Wv3y4ZBWkkXlD3m7vtHIiWBqsMHspWSfie1s07mM45i/bBCf6YBMgz17FUxIXwmZA== + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.21.0.tgz#40d71aaec5b696e58743889ce6d5bf2593f9a23d" + integrity sha512-on1jTzIHc5DyWhRP+xpf+zrgrREXcHBH4EDAfaB5mIG7TWpKxNXooQ1JCylaPsswZUv4wGnVTinr4HrBdGARAQ== dependencies: - "@opentelemetry/core" "1.17.1" - "@opentelemetry/resources" "1.17.1" + "@opentelemetry/core" "1.21.0" + "@opentelemetry/resources" "1.21.0" lodash.merge "^4.6.2" -"@opentelemetry/sdk-trace-base@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.0.tgz#05a21763c9efa72903c20b8930293cdde344b681" - integrity sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ== +"@opentelemetry/sdk-trace-base@1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.20.0.tgz#1771bf7a214924fe1f27ef50395f763b65aae220" + integrity sha512-BAIZ0hUgnhdb3OBQjn1FKGz/Iwie4l+uOMKklP7FGh7PTqEAbbzDNMJKaZQh6KepF7Fq+CZDRKslD3yrYy2Tzw== dependencies: - "@opentelemetry/core" "1.17.0" - "@opentelemetry/resources" "1.17.0" - "@opentelemetry/semantic-conventions" "1.17.0" + "@opentelemetry/core" "1.20.0" + "@opentelemetry/resources" "1.20.0" + "@opentelemetry/semantic-conventions" "1.20.0" -"@opentelemetry/sdk-trace-base@1.17.1", "@opentelemetry/sdk-trace-base@^1.17.1", "@opentelemetry/sdk-trace-base@~1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.1.tgz#8ede213df8b0c957028a869c66964e535193a4fd" - integrity sha512-pfSJJSjZj5jkCJUQZicSpzN8Iz9UKMryPWikZRGObPnJo6cUSoKkjZh6BM3j+D47G4olMBN+YZKYqkFM1L6zNA== +"@opentelemetry/sdk-trace-base@1.21.0", "@opentelemetry/sdk-trace-base@^1.17.1": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.21.0.tgz#ffad912e453a92044fb220bd5d2f6743bf37bb8a" + integrity sha512-yrElGX5Fv0umzp8Nxpta/XqU71+jCAyaLk34GmBzNcrW43nqbrqvdPs4gj4MVy/HcTjr6hifCDCYA3rMkajxxA== dependencies: - "@opentelemetry/core" "1.17.1" - "@opentelemetry/resources" "1.17.1" - "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/core" "1.21.0" + "@opentelemetry/resources" "1.21.0" + "@opentelemetry/semantic-conventions" "1.21.0" "@opentelemetry/sdk-trace-node@^1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.17.1.tgz#746c197ad54a8e0cdb24a4b257d33dc3a04493c1" - integrity sha512-J56DaG4cusjw5crpI7x9rv4bxDF27DtKYGxXJF56KIvopbNKpdck5ZWXBttEyqgAVPDwHMAXWDL1KchHzF0a3A== - dependencies: - "@opentelemetry/context-async-hooks" "1.17.1" - "@opentelemetry/core" "1.17.1" - "@opentelemetry/propagator-b3" "1.17.1" - "@opentelemetry/propagator-jaeger" "1.17.1" - "@opentelemetry/sdk-trace-base" "1.17.1" + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.21.0.tgz#20599f42a6b59bf71c64ef8630d28464e6e18f2a" + integrity sha512-1pdm8jnqs+LuJ0Bvx6sNL28EhC8Rv7NYV8rnoXq3GIQo7uOHBDAFSj7makAfbakrla7ecO1FRfI8emnR4WvhYA== + dependencies: + "@opentelemetry/context-async-hooks" "1.21.0" + "@opentelemetry/core" "1.21.0" + "@opentelemetry/propagator-b3" "1.21.0" + "@opentelemetry/propagator-jaeger" "1.21.0" + "@opentelemetry/sdk-trace-base" "1.21.0" semver "^7.5.2" -"@opentelemetry/semantic-conventions@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.0.tgz#af10baa9f05ce1e64a14065fc138b5739bfb65f6" - integrity sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA== +"@opentelemetry/semantic-conventions@1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.20.0.tgz#4d9b88188e18056a218644ea30fae130a7857766" + integrity sha512-3zLJJCgTKYpbqFX8drl8hOCHtdchELC+kGqlVcV4mHW1DiElTtv1Nt9EKBptTd1IfL56QkuYnWJ3DeHd2Gtu/A== -"@opentelemetry/semantic-conventions@1.17.1", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.17.1", "@opentelemetry/semantic-conventions@~1.17.1": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.1.tgz#93d219935e967fbb9aa0592cc96b2c0ec817a56f" - integrity sha512-xbR2U+2YjauIuo42qmE8XyJK6dYeRMLJuOlUP5SO4auET4VtOHOzgkRVOq+Ik18N+Xf3YPcqJs9dZMiDddz1eQ== +"@opentelemetry/semantic-conventions@1.21.0", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.17.1": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz#83f7479c524ab523ac2df702ade30b9724476c72" + integrity sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g== "@opentelemetry/semantic-conventions@^0.12.0": version "0.12.0" @@ -5014,14 +5023,14 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53" integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w== -"@prisma/instrumentation@5.4.2": - version "5.4.2" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.4.2.tgz#e1615cb50485f029a47e79378d3edac483d6a5f3" - integrity sha512-VSBfo0VS6aY1fIuMBbeLBaTmmgZxszMn2DvHRnGzEnqD/B9/Yfiu96+c0SKuYr7VkuXlbmt5dpbkJutvuJzZBQ== +"@prisma/instrumentation@5.9.0": + version "5.9.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.9.0.tgz#b36751a965a320a099f5665854340c5739f9bbe0" + integrity sha512-VjLZQM/Gv5EgN8l7T+VH5nbSYbl25tkkQJCMyrV+ajY6wRYwsUY3WPEzqdYe/eB3zcfr6+rUN+Cp919scUYt/A== dependencies: - "@opentelemetry/api" "1.6.0" - "@opentelemetry/instrumentation" "0.43.0" - "@opentelemetry/sdk-trace-base" "1.17.0" + "@opentelemetry/api" "1.7.0" + "@opentelemetry/instrumentation" "0.47.0" + "@opentelemetry/sdk-trace-base" "1.20.0" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -6289,16 +6298,6 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express-serve-static-core@^4.17.33": - version "4.17.36" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.36.tgz#baa9022119bdc05a4adfe740ffc97b5f9360e545" - integrity sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - "@types/express@4.17.14", "@types/express@^4.17.14", "@types/express@^4.17.2": version "4.17.14" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" @@ -6309,16 +6308,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@4.17.18": - version "4.17.18" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.18.tgz#efabf5c4495c1880df1bdffee604b143b29c4a95" - integrity sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - "@types/fs-extra@^5.0.5": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" @@ -6804,14 +6793,6 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== -"@types/send@*": - version "0.17.1" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" - integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - "@types/serve-static@*": version "1.13.9" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" @@ -18005,6 +17986,26 @@ import-in-the-middle@1.4.2: cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" +import-in-the-middle@1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.1.tgz#3e111ff79c639d0bde459bd7ba29dd9fdf357364" + integrity sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg== + dependencies: + acorn "^8.8.2" + acorn-import-assertions "^1.9.0" + cjs-module-lexer "^1.2.2" + module-details-from-path "^1.0.3" + +import-in-the-middle@^1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.3.tgz#ffa784cdd57a47d2b68d2e7dd33070ff06baee43" + integrity sha512-R2I11NRi0lI3jD2+qjqyVlVEahsejw7LDnYEbGb47QEFjczE3bZYsmWheCTQA+LFs2DzOQxR7Pms7naHW1V4bQ== + dependencies: + acorn "^8.8.2" + acorn-import-assertions "^1.9.0" + cjs-module-lexer "^1.2.2" + module-details-from-path "^1.0.3" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" From c1848cd8c856471b0f65e349f4822fc5a0ff59c8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Feb 2024 08:43:40 +0100 Subject: [PATCH 35/68] feat(bundles): Add pluggable integrations on CDN to `Sentry` namespace (#10452) Previously, they were only put on `Sentry.Integrations.XXX`, now you can do e.g. `Sentry.httpClientIntegration()`. While at it, I also added a browser integration test for this. I also made the way we do this more future proof, as in v8 this will not be imported anymore from `@sentry/integrations` (which we rely on right now), plus the heuristic used to rely on integration name === filename. Now, there is a manual map of imported method names to a CDN bundle file name. --- .../httpclient/httpClientIntegration/init.js | 11 ++++ .../httpClientIntegration/subject.js | 8 +++ .../httpclient/httpClientIntegration/test.ts | 64 +++++++++++++++++++ .../utils/generatePlugin.ts | 32 ++++++++-- dev-packages/rollup-utils/bundleHelpers.mjs | 1 + 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js new file mode 100644 index 000000000000..07bc4a5b351e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; +import { httpClientIntegration } from '@sentry/integrations'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [httpClientIntegration()], + tracesSampleRate: 1, + sendDefaultPii: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js new file mode 100644 index 000000000000..7a2e3cdd28c0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/subject.js @@ -0,0 +1,8 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://localhost:7654/foo', true); +xhr.withCredentials = true; +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts new file mode 100644 index 000000000000..8bf8efa34cc4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/httpClientIntegration/test.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('works with httpClientIntegration', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: false, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'GET', + headers: { + accept: 'application/json', + cache: 'no-cache', + 'content-type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 1258c684492d..cf2816ab0033 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -22,6 +22,27 @@ const useCompiledModule = bundleKey === 'esm' || bundleKey === 'cjs'; const useBundleOrLoader = bundleKey && !useCompiledModule; const useLoader = bundleKey.startsWith('loader'); +// These are imports that, when using CDN bundles, are not included in the main CDN bundle. +// In this case, if we encounter this import, we want to add this CDN bundle file instead +const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { + httpClientIntegration: 'httpclient', + HttpClient: 'httpclient', + captureConsoleIntegration: 'captureconsole', + CaptureConsole: 'captureconsole', + debugIntegration: 'debug', + Debug: 'debug', + rewriteFramesIntegration: 'rewriteframes', + RewriteFrames: 'rewriteframes', + contextLinesIntegration: 'contextlines', + ContextLines: 'contextlines', + extraErrorDataIntegration: 'extraerrordata', + ExtraErrorData: 'extraerrordata', + reportingObserverIntegration: 'reportingobserver', + ReportingObserver: 'reportingobserver', + sessionTimingIntegration: 'sessiontiming', + SessionTiming: 'sessiontiming', +}; + const BUNDLE_PATHS: Record> = { browser: { cjs: 'build/npm/cjs/index.js', @@ -149,8 +170,8 @@ class SentryScenarioGenerationPlugin { '@sentry/browser': 'Sentry', '@sentry/tracing': 'Sentry', '@sentry/replay': 'Sentry', - '@sentry/integrations': 'Sentry.Integrations', - '@sentry/wasm': 'Sentry.Integrations', + '@sentry/integrations': 'Sentry', + '@sentry/wasm': 'Sentry', } : {}; @@ -161,8 +182,11 @@ class SentryScenarioGenerationPlugin { parser.hooks.import.tap( this._name, (statement: { specifiers: [{ imported: { name: string } }] }, source: string) => { - if (source === '@sentry/integrations') { - this.requiredIntegrations.push(statement.specifiers[0].imported.name.toLowerCase()); + const imported = statement.specifiers?.[0]?.imported?.name; + + if (imported && IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS[imported]) { + const bundleName = IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS[imported]; + this.requiredIntegrations.push(bundleName); } else if (source === '@sentry/wasm') { this.requiresWASMIntegration = true; } diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index b6ca7c8fcbc7..66bded3b62de 100644 --- a/dev-packages/rollup-utils/bundleHelpers.mjs +++ b/dev-packages/rollup-utils/bundleHelpers.mjs @@ -82,6 +82,7 @@ export function makeBaseBundleConfig(options) { ' for (var key in exports) {', ' if (Object.prototype.hasOwnProperty.call(exports, key)) {', ' __window.Sentry.Integrations[key] = exports[key];', + ' __window.Sentry[key] = exports[key];', ' }', ' }', ].join('\n'), From 649101ddaa80ffb0f8ba9378c8fb830eb2a7b2e7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Feb 2024 11:56:38 +0100 Subject: [PATCH 36/68] feat(node-experimental): Add koa integration (#10451) This should auto-instrument Koa performance. We still needs tests for this at some point. cc @timfish / @onurtemizkan The main reason to add this is that we have docs for this on docs.sentry.com, and we need a way to properly replace/update them (and they currently rely a lot on `startTransaction()` etc.) With this we can eventually just update this to use the default koa integration we provide. --- packages/node-experimental/package.json | 1 + .../getAutoPerformanceIntegrations.ts | 2 + .../node-experimental/src/integrations/koa.ts | 17 +++ yarn.lock | 114 ++++++++++++++++-- 4 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 packages/node-experimental/src/integrations/koa.ts diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 3822da240dfb..cedda1f83426 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -38,6 +38,7 @@ "@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", diff --git a/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts b/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts index 77d772ce005b..ce3773fec1eb 100644 --- a/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts +++ b/packages/node-experimental/src/integrations/getAutoPerformanceIntegrations.ts @@ -4,6 +4,7 @@ import { expressIntegration } from './express'; import { fastifyIntegration } from './fastify'; import { graphqlIntegration } from './graphql'; import { hapiIntegration } from './hapi'; +import { koaIntegration } from './koa'; import { mongoIntegration } from './mongo'; import { mongooseIntegration } from './mongoose'; import { mysqlIntegration } from './mysql'; @@ -28,5 +29,6 @@ export function getAutoPerformanceIntegrations(): Integration[] { prismaIntegration(), nestIntegration(), hapiIntegration(), + koaIntegration(), ]; } diff --git a/packages/node-experimental/src/integrations/koa.ts b/packages/node-experimental/src/integrations/koa.ts new file mode 100644 index 000000000000..2d85703c054a --- /dev/null +++ b/packages/node-experimental/src/integrations/koa.ts @@ -0,0 +1,17 @@ +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; + +const _koaIntegration = (() => { + return { + name: 'Koa', + setupOnce() { + registerInstrumentations({ + instrumentations: [new KoaInstrumentation()], + }); + }, + }; +}) satisfies IntegrationFn; + +export const koaIntegration = defineIntegration(_koaIntegration); diff --git a/yarn.lock b/yarn.lock index 1acf15085acd..1d97ef968f5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4788,6 +4788,17 @@ "@opentelemetry/semantic-conventions" "1.21.0" semver "^7.5.2" +"@opentelemetry/instrumentation-koa@0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.37.0.tgz#f12e608afb7b58cee0f27abb3c2a166ea8596c68" + integrity sha512-EfuGv1RJCSZh77dDc3PtvZXGwcsTufn9tU6T9VOTFcxovpyJ6w0og73eD0D02syR8R+kzv6rg1TeS8+lj7pyrQ== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.48.0" + "@opentelemetry/semantic-conventions" "^1.0.0" + "@types/koa" "2.14.0" + "@types/koa__router" "12.0.3" + "@opentelemetry/instrumentation-mongodb@0.39.0": version "0.39.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.39.0.tgz#31bc92c137b578219bfaf4d15c7f247bc8d3b2c6" @@ -5560,15 +5571,6 @@ "@sentry/types" "7.93.0" "@sentry/utils" "7.93.0" -"@sentry/hub@7.93.0": - version "7.93.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.93.0.tgz#3db26f74dc1269650fa9ec553f0837af5caa0d30" - integrity sha512-gfPyT3DFGYYM5d+CHiS0YJHkIJHa8MKSfl32RkCMA5KQr9RF0H+GR5LZCinQOWq43fpsncSLyuoMfBjgbMLN+w== - dependencies: - "@sentry/core" "7.93.0" - "@sentry/types" "7.93.0" - "@sentry/utils" "7.93.0" - "@sentry/node@7.93.0": version "7.93.0" resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.93.0.tgz#7786d05d1e3e984207a866b07df1bf891355892e" @@ -5892,6 +5894,13 @@ "@tufjs/canonical-json" "1.0.0" minimatch "^9.0.0" +"@types/accepts@*": + version "1.3.7" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.7.tgz#3b98b1889d2b2386604c2bbbe62e4fb51e95b265" + integrity sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ== + dependencies: + "@types/node" "*" + "@types/accepts@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" @@ -6011,6 +6020,11 @@ dependencies: "@types/node" "*" +"@types/content-disposition@*": + version "0.5.8" + 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" @@ -6026,6 +6040,16 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== +"@types/cookies@*": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.9.0.tgz#a2290cfb325f75f0f28720939bee854d4142aee2" + integrity sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + "@types/cors@2.8.12", "@types/cors@^2.8.12": version "2.8.12" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" @@ -6298,6 +6322,26 @@ "@types/qs" "*" "@types/range-parser" "*" +"@types/express-serve-static-core@^4.17.33": + version "4.17.42" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz#2a276952acc73d1b8dc63fd4210647abbc553a71" + integrity sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/express@4.17.14", "@types/express@^4.17.14", "@types/express@^4.17.2": version "4.17.14" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" @@ -6421,6 +6465,16 @@ resolved "https://registry.yarnpkg.com/@types/htmlbars-inline-precompile/-/htmlbars-inline-precompile-1.0.1.tgz#de564513fabb165746aecd76369c87bd85e5bbb4" integrity sha512-sVD2e6QAAHW0Y6Btse+tTA9k9g0iKm87wjxRsgZRU5EwSooz80tenbV+fA+f2BI2g0G2CqxsS1rIlwQCtPRQow== +"@types/http-assert@*": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.5.tgz#dfb1063eb7c240ee3d3fe213dac5671cfb6a8dbf" + integrity sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g== + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -6479,6 +6533,39 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/keygrip@*": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.6.tgz#1749535181a2a9b02ac04a797550a8787345b740" + integrity sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ== + +"@types/koa-compose@*": + version "3.2.8" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.8.tgz#dec48de1f6b3d87f87320097686a915f1e954b57" + integrity sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA== + dependencies: + "@types/koa" "*" + +"@types/koa@*", "@types/koa@2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.14.0.tgz#8939e8c3b695defc12f2ef9f38064509e564be18" + integrity sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA== + dependencies: + "@types/accepts" "*" + "@types/content-disposition" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/http-errors" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/koa__router@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-12.0.3.tgz#3fb74ea1991cadd6c6712b6106657aa6e64afca4" + integrity sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw== + dependencies: + "@types/koa" "*" + "@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -6793,6 +6880,14 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + "@types/serve-static@*": version "1.13.9" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" @@ -12196,6 +12291,7 @@ cron@^3.1.6: dependencies: "@types/luxon" "~3.3.0" luxon "~3.4.0" + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" From 0e52c0798f4bffa3f2bf3fa86c1c916090740fd1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 2 Feb 2024 13:46:49 +0100 Subject: [PATCH 37/68] feat(sveltekit): Add custom `browserTracingIntegration()` (#10450) Deprecates SvelteKit's `BrowserTracing` integration in favour of the new `browserTracingIntegration` functional integration. The new integration now also directly initializes the routing instrumentation. --- .../src/client/browserTracingIntegration.ts | 150 ++++++++- packages/sveltekit/src/client/index.ts | 1 + packages/sveltekit/src/client/router.ts | 3 + packages/sveltekit/src/client/sdk.ts | 12 +- .../client/browserTracingIntegration.test.ts | 285 ++++++++++++++++++ packages/sveltekit/test/client/router.test.ts | 14 +- packages/sveltekit/test/client/sdk.test.ts | 4 +- 7 files changed, 457 insertions(+), 12 deletions(-) create mode 100644 packages/sveltekit/test/client/browserTracingIntegration.test.ts diff --git a/packages/sveltekit/src/client/browserTracingIntegration.ts b/packages/sveltekit/src/client/browserTracingIntegration.ts index 9968f8b6de5f..ffabc2a374a7 100644 --- a/packages/sveltekit/src/client/browserTracingIntegration.ts +++ b/packages/sveltekit/src/client/browserTracingIntegration.ts @@ -1,14 +1,162 @@ -import { BrowserTracing as OriginalBrowserTracing } from '@sentry/svelte'; +import { navigating, page } from '$app/stores'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { + BrowserTracing as OriginalBrowserTracing, + WINDOW, + browserTracingIntegration as originalBrowserTracingIntegration, + getActiveSpan, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + startInactiveSpan, +} from '@sentry/svelte'; +import type { Client, Integration, Span } from '@sentry/types'; import { svelteKitRoutingInstrumentation } from './router'; /** * A custom BrowserTracing integration for Sveltekit. + * + * @deprecated use `browserTracingIntegration()` instead. The new `browserTracingIntegration()` + * includes SvelteKit-specific routing instrumentation out of the box. Therefore there's no need + * to pass in `svelteKitRoutingInstrumentation` anymore. */ export class BrowserTracing extends OriginalBrowserTracing { public constructor(options?: ConstructorParameters[0]) { super({ + // eslint-disable-next-line deprecation/deprecation routingInstrumentation: svelteKitRoutingInstrumentation, ...options, }); } } + +/** + * A custom `BrowserTracing` integration for SvelteKit. + */ +export function browserTracingIntegration( + options: Parameters[0] = {}, +): Integration { + const integration = { + ...originalBrowserTracingIntegration({ + ...options, + instrumentNavigation: false, + instrumentPageLoad: false, + }), + }; + + return { + ...integration, + afterAllSetup: client => { + integration.afterAllSetup(client); + + if (options.instrumentPageLoad !== false) { + _instrumentPageload(client); + } + + if (options.instrumentNavigation !== false) { + _instrumentNavigations(client); + } + }, + }; +} + +function _instrumentPageload(client: Client): void { + const initialPath = WINDOW && WINDOW.location && WINDOW.location.pathname; + + startBrowserTracingPageLoadSpan(client, { + name: initialPath, + op: 'pageload', + description: initialPath, + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }); + + const pageloadSpan = getActiveSpan(); + + page.subscribe(page => { + if (!page) { + return; + } + + const routeId = page.route && page.route.id; + + if (pageloadSpan && routeId) { + pageloadSpan.updateName(routeId); + pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } + }); +} + +/** + * Use the `navigating` store to start a transaction on navigations. + */ +function _instrumentNavigations(client: Client): void { + let routingSpan: Span | undefined; + let activeSpan: Span | undefined; + + navigating.subscribe(navigation => { + if (!navigation) { + // `navigating` emits a 'null' value when the navigation is completed. + // So in this case, we can finish the routing span. If the transaction was an IdleTransaction, + // it will finish automatically and if it was user-created users also need to finish it. + if (routingSpan) { + routingSpan.end(); + routingSpan = undefined; + } + return; + } + + const from = navigation.from; + const to = navigation.to; + + // for the origin we can fall back to window.location.pathname because in this emission, it still is set to the origin path + const rawRouteOrigin = (from && from.url.pathname) || (WINDOW && WINDOW.location && WINDOW.location.pathname); + + const rawRouteDestination = to && to.url.pathname; + + // We don't want to create transactions for navigations of same origin and destination. + // We need to look at the raw URL here because parameterized routes can still differ in their raw parameters. + if (rawRouteOrigin === rawRouteDestination) { + return; + } + + const parameterizedRouteOrigin = from && from.route.id; + const parameterizedRouteDestination = to && to.route.id; + + activeSpan = getActiveSpan(); + + if (!activeSpan) { + startBrowserTracingNavigationSpan(client, { + name: parameterizedRouteDestination || rawRouteDestination || 'unknown', + op: 'navigation', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedRouteDestination ? 'route' : 'url', + }, + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + }); + activeSpan = getActiveSpan(); + } + + if (activeSpan) { + if (routingSpan) { + // If a routing span is still open from a previous navigation, we finish it. + routingSpan.end(); + } + routingSpan = startInactiveSpan({ + op: 'ui.sveltekit.routing', + name: 'SvelteKit Route Change', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + }, + }); + activeSpan.setAttribute('sentry.sveltekit.navigation.from', parameterizedRouteOrigin || undefined); + } + }); +} diff --git a/packages/sveltekit/src/client/index.ts b/packages/sveltekit/src/client/index.ts index f60a353d8b1d..558526b1f318 100644 --- a/packages/sveltekit/src/client/index.ts +++ b/packages/sveltekit/src/client/index.ts @@ -3,3 +3,4 @@ export * from '@sentry/svelte'; export { init } from './sdk'; export { handleErrorWithSentry } from './handleError'; export { wrapLoadWithSentry } from './load'; +export { browserTracingIntegration } from './browserTracingIntegration'; diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index 2b36d4adb4f2..593eeb97b1a2 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -17,6 +17,9 @@ const DEFAULT_TAGS = { * @param startTransactionFn the function used to start (idle) transactions * @param startTransactionOnPageLoad controls if pageload transactions should be created (defaults to `true`) * @param startTransactionOnLocationChange controls if navigation transactions should be created (defauls to `true`) + * + * @deprecated use `browserTracingIntegration()` instead which includes SvelteKit-specific routing instrumentation out of the box. + * Therefore, this function will be removed in v8. */ export function svelteKitRoutingInstrumentation( startTransactionFn: (context: TransactionContext) => T | undefined, diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 920b2db75193..b0dc7ee6af2d 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -4,7 +4,10 @@ import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/ import { WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte'; import type { Integration } from '@sentry/types'; -import { BrowserTracing } from './browserTracingIntegration'; +import { + BrowserTracing, + browserTracingIntegration as svelteKitBrowserTracingIntegration, +} from './browserTracingIntegration'; type WindowWithSentryFetchProxy = typeof WINDOW & { _sentryFetchProxy?: typeof fetch; @@ -64,6 +67,7 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void { function isNewBrowserTracingIntegration( integration: Integration, ): integration is Integration & { options?: Parameters[0] } { + // eslint-disable-next-line deprecation/deprecation return !!integration.afterAllSetup && !!(integration as BrowserTracing).options; } @@ -77,15 +81,19 @@ function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Inte // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one if (isNewBrowserTracingIntegration(browserTracing)) { const { options } = browserTracing; + // eslint-disable-next-line deprecation/deprecation integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); } // If BrowserTracing was added, but it is not our forked version, // replace it with our forked version with the same options + // eslint-disable-next-line deprecation/deprecation if (!(browserTracing instanceof BrowserTracing)) { + // eslint-disable-next-line deprecation/deprecation const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options; // This option is overwritten by the custom integration delete options.routingInstrumentation; + // eslint-disable-next-line deprecation/deprecation integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); } @@ -97,7 +105,7 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] | undefi // will get treeshaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { if (hasTracingEnabled(options)) { - return [...getDefaultSvelteIntegrations(options), new BrowserTracing()]; + return [...getDefaultSvelteIntegrations(options), svelteKitBrowserTracingIntegration()]; } } diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts new file mode 100644 index 000000000000..83984c0b19f5 --- /dev/null +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -0,0 +1,285 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import type { Span } from '@sentry/types'; +import { writable } from 'svelte/store'; +import { vi } from 'vitest'; + +import { navigating, page } from '$app/stores'; + +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { browserTracingIntegration } from '../../src/client'; + +import * as SentrySvelte from '@sentry/svelte'; + +// we have to overwrite the global mock from `vitest.setup.ts` here to reset the +// `navigating` store for each test. +vi.mock('$app/stores', async () => { + return { + get navigating() { + return navigatingStore; + }, + page: writable(), + }; +}); + +let navigatingStore = writable(); + +describe('browserTracingIntegration', () => { + const svelteBrowserTracingIntegrationSpy = vi.spyOn(SentrySvelte, 'browserTracingIntegration'); + + let createdRootSpan: Partial | undefined; + + // @ts-expect-error - only returning a partial span here, that's fine + vi.spyOn(SentrySvelte, 'getActiveSpan').mockImplementation(() => { + return createdRootSpan; + }); + + const startBrowserTracingPageLoadSpanSpy = vi + .spyOn(SentrySvelte, 'startBrowserTracingPageLoadSpan') + .mockImplementation((_client, txnCtx) => { + createdRootSpan = { + ...txnCtx, + updateName: vi.fn(), + setAttribute: vi.fn(), + startChild: vi.fn().mockImplementation(ctx => { + return { ...mockedRoutingSpan, ...ctx }; + }), + setTag: vi.fn(), + }; + }); + + const startBrowserTracingNavigationSpanSpy = vi + .spyOn(SentrySvelte, 'startBrowserTracingNavigationSpan') + .mockImplementation((_client, txnCtx) => { + createdRootSpan = { + ...txnCtx, + updateName: vi.fn(), + setAttribute: vi.fn(), + setTag: vi.fn(), + }; + }); + + const fakeClient = { getOptions: () => undefined }; + + const mockedRoutingSpan = { + end: () => {}, + }; + + const routingSpanEndSpy = vi.spyOn(mockedRoutingSpan, 'end'); + + // @ts-expect-error - mockedRoutingSpan is not a complete Span, that's fine + const startInactiveSpanSpy = vi.spyOn(SentrySvelte, 'startInactiveSpan').mockImplementation(() => mockedRoutingSpan); + + beforeEach(() => { + createdRootSpan = undefined; + navigatingStore = writable(); + vi.clearAllMocks(); + }); + + it('implements required hooks', () => { + const integration = browserTracingIntegration(); + expect(integration.name).toEqual('BrowserTracing'); + expect(integration.setupOnce).toBeDefined(); + expect(integration.afterAllSetup).toBeDefined(); + }); + + it('passes on the options to the original integration', () => { + browserTracingIntegration({ enableLongTask: true, idleTimeout: 4242 }); + expect(svelteBrowserTracingIntegrationSpy).toHaveBeenCalledTimes(1); + expect(svelteBrowserTracingIntegrationSpy).toHaveBeenCalledWith({ + enableLongTask: true, + idleTimeout: 4242, + instrumentNavigation: false, + instrumentPageLoad: false, + }); + }); + + it('always disables `instrumentNavigation` and `instrumentPageLoad` in the original integration', () => { + browserTracingIntegration({ instrumentNavigation: true, instrumentPageLoad: true }); + expect(svelteBrowserTracingIntegrationSpy).toHaveBeenCalledTimes(1); + // This is fine and expected because we don't want to start the default instrumentation + // SvelteKit's browserTracingIntegration takes care of instrumenting pageloads and navigations on its own. + expect(svelteBrowserTracingIntegrationSpy).toHaveBeenCalledWith({ + instrumentNavigation: false, + instrumentPageLoad: false, + }); + }); + + it("starts a pageload span when it's called with default params", () => { + const integration = browserTracingIntegration(); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledTimes(1); + expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(fakeClient, { + name: '/', + op: 'pageload', + description: '/', + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }); + + // 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' } }); + + // This should update the transaction name with the parameterized route: + expect(createdRootSpan?.updateName).toHaveBeenCalledTimes(1); + expect(createdRootSpan?.updateName).toHaveBeenCalledWith('testRoute'); + expect(createdRootSpan?.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it("doesn't start a pageload span if `instrumentPageLoad` is false", () => { + const integration = browserTracingIntegration({ + instrumentPageLoad: false, + }); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledTimes(0); + }); + + it("doesn't start a navigation span when `instrumentNavigation` is false", () => { + const integration = browserTracingIntegration({ + instrumentNavigation: false, + }); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle + // @ts-expect-error - page is a writable but the types say it's just readable + navigating.set({ + from: { route: { id: '/users' }, url: { pathname: '/users' } }, + to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, + }); + + // This should update the transaction name with the parameterized route: + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledTimes(0); + }); + + it('starts a navigation span when `startTransactionOnLocationChange` is true', () => { + const integration = browserTracingIntegration({ + instrumentPageLoad: false, + }); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle + // @ts-expect-error - page is a writable but the types say it's just readable + navigating.set({ + from: { route: { id: '/users' }, url: { pathname: '/users' } }, + to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, + }); + + // This should update the transaction name with the parameterized route: + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledTimes(1); + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledWith(fakeClient, { + name: '/users/[id]', + op: 'navigation', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + }); + + // eslint-disable-next-line deprecation/deprecation + expect(startInactiveSpanSpy).toHaveBeenCalledWith({ + op: 'ui.sveltekit.routing', + name: 'SvelteKit Route Change', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + }, + }); + + // eslint-disable-next-line deprecation/deprecation + expect(createdRootSpan?.setAttribute).toHaveBeenCalledWith('sentry.sveltekit.navigation.from', '/users'); + + // We emit `null` here to simulate the end of the navigation lifecycle + // @ts-expect-error - page is a writable but the types say it's just readable + navigating.set(null); + + expect(routingSpanEndSpy).toHaveBeenCalledTimes(1); + }); + + describe('handling same origin and destination navigations', () => { + it("doesn't start a navigation span if the raw navigation origin and destination are equal", () => { + const integration = browserTracingIntegration({ + instrumentPageLoad: false, + }); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle + // @ts-expect-error - page is a writable but the types say it's just readable + navigating.set({ + from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, + to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, + }); + + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledTimes(0); + }); + + it('starts a navigation transaction if the raw navigation origin and destination are not equal', () => { + const integration = browserTracingIntegration({ + instrumentPageLoad: false, + }); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + // @ts-expect-error - page is a writable but the types say it's just readable + navigating.set({ + from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, + to: { route: { id: '/users/[id]' }, url: { pathname: '/users/223412' } }, + }); + + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledTimes(1); + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledWith(fakeClient, { + name: '/users/[id]', + op: 'navigation', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + }, + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + }); + + // eslint-disable-next-line deprecation/deprecation + expect(startInactiveSpanSpy).toHaveBeenCalledWith({ + op: 'ui.sveltekit.routing', + name: 'SvelteKit Route Change', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + }, + }); + + // eslint-disable-next-line deprecation/deprecation + expect(createdRootSpan?.setAttribute).toHaveBeenCalledWith('sentry.sveltekit.navigation.from', '/users/[id]'); + }); + + it('falls back to `window.location.pathname` to determine the raw origin', () => { + const integration = browserTracingIntegration({ + instrumentPageLoad: false, + }); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + + // window.location.pathame is "/" in tests + + // @ts-expect-error - page is a writable but the types say it's just readable + navigating.set({ + to: { route: {}, url: { pathname: '/' } }, + }); + + expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 29037c28461f..a359a9dedbf0 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -49,6 +49,7 @@ describe('sveltekitRoutingInstrumentation', () => { }); it("starts a pageload transaction when it's called with default params", () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction); expect(mockedStartTransaction).toHaveBeenCalledTimes(1); @@ -66,7 +67,6 @@ describe('sveltekitRoutingInstrumentation', () => { }); // We emit an update to the `page` store to simulate the SvelteKit router lifecycle - // @ts-expect-error This is fine because we testUtils/stores.ts defines `page` as a writable store page.set({ route: { id: 'testRoute' } }); // This should update the transaction name with the parameterized route: @@ -76,15 +76,16 @@ describe('sveltekitRoutingInstrumentation', () => { }); it("doesn't start a pageload transaction if `startTransactionOnPageLoad` is false", () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction, false); expect(mockedStartTransaction).toHaveBeenCalledTimes(0); }); it("doesn't start a navigation transaction when `startTransactionOnLocationChange` is false", () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction, false, false); // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle - // @ts-expect-error This is fine because we testUtils/stores.ts defines `navigating` as a writable store navigating.set({ from: { route: { id: '/users' }, url: { pathname: '/users' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, @@ -95,10 +96,10 @@ describe('sveltekitRoutingInstrumentation', () => { }); it('starts a navigation transaction when `startTransactionOnLocationChange` is true', () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle - // @ts-expect-error This is fine because we testUtils/stores.ts defines `navigating` as a writable store navigating.set({ from: { route: { id: '/users' }, url: { pathname: '/users' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, @@ -127,7 +128,6 @@ describe('sveltekitRoutingInstrumentation', () => { expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users'); // We emit `null` here to simulate the end of the navigation lifecycle - // @ts-expect-error this is fine navigating.set(null); expect(routingSpanFinishSpy).toHaveBeenCalledTimes(1); @@ -135,10 +135,10 @@ describe('sveltekitRoutingInstrumentation', () => { describe('handling same origin and destination navigations', () => { it("doesn't start a navigation transaction if the raw navigation origin and destination are equal", () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle - // @ts-expect-error This is fine because we testUtils/stores.ts defines `navigating` as a writable store navigating.set({ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, @@ -148,9 +148,9 @@ describe('sveltekitRoutingInstrumentation', () => { }); it('starts a navigation transaction if the raw navigation origin and destination are not equal', () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); - // @ts-expect-error This is fine navigating.set({ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/223412' } }, @@ -179,11 +179,11 @@ describe('sveltekitRoutingInstrumentation', () => { }); it('falls back to `window.location.pathname` to determine the raw origin', () => { + // eslint-disable-next-line deprecation/deprecation svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); // window.location.pathame is "/" in tests - // @ts-expect-error This is fine navigating.set({ to: { route: {}, url: { pathname: '/' } }, }); diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index 4b0afb85bcd8..c6eca47e5e79 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -82,7 +82,6 @@ describe('Sentry client SDK', () => { // This is the closest we can get to unit-testing the `__SENTRY_TRACING__` tree-shaking guard // IRL, the code to add the integration would most likely be removed by the bundler. - // @ts-expect-error this is fine in the test globalThis.__SENTRY_TRACING__ = false; init({ @@ -93,7 +92,6 @@ describe('Sentry client SDK', () => { const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(browserTracing).toBeUndefined(); - // @ts-expect-error this is fine in the test delete globalThis.__SENTRY_TRACING__; }); @@ -113,6 +111,7 @@ describe('Sentry client SDK', () => { expect(options.finalTimeout).toEqual(10); // But we force the routing instrumentation to be ours + // eslint-disable-next-line deprecation/deprecation expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation); }); @@ -132,6 +131,7 @@ describe('Sentry client SDK', () => { expect(options.finalTimeout).toEqual(10); // But we force the routing instrumentation to be ours + // eslint-disable-next-line deprecation/deprecation expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation); }); }); From cd3a7444aeb6d203281fb30007200a965863182f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Feb 2024 15:26:58 +0100 Subject: [PATCH 38/68] feat: Export `setHttpStatus` from all packages (#10475) This replaces an old API but was not exported anywhere. We introduced this here: https://github.com/getsentry/sentry-javascript/pull/10268/files --- packages/astro/src/index.server.ts | 1 + packages/browser/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/node-experimental/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/index.server.ts | 1 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 50a77fff599c..98e5486894db 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -49,6 +49,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index cda5f65a800e..4518f0174f35 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -72,6 +72,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, makeMultiplexedTransport, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index ffe316fd30ec..b51083052c8b 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -69,6 +69,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f5ed9651bf94..d42ad97fedb8 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -68,6 +68,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 9338fae2183d..b5ffeb6de0c9 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -81,6 +81,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, captureCheckIn, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index fc0edc005400..8d0a82ecbfe6 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -68,6 +68,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index eb5adacf7baf..674beed61ee6 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -53,6 +53,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 84504f7566a2..24ee21115f0b 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -41,6 +41,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation makeMain, setCurrentClient, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index c01311520695..7a886334cfbc 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -47,6 +47,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 8937d35d38c8..8288c8ca5374 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -68,6 +68,7 @@ export { // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, getSpanStatusFromHttpCode, + setHttpStatus, // eslint-disable-next-line deprecation/deprecation trace, withScope, From 3b2b18cd95426952b1eb4736499e0810bab7fd28 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Feb 2024 15:54:50 +0100 Subject: [PATCH 39/68] feat(replay): Enforce masking of credit card fields (#10472) This bumps our rrweb-fork to 2.11.0, which mainly includes an improvement to avoid capturing credit card inputs. See: https://github.com/getsentry/rrweb/releases/tag/2.11.0 Fixes https://github.com/getsentry/sentry-javascript/issues/10258 I also added a test in replay itself to verify that this works as expected! --- .../browser-integration-tests/package.json | 2 +- .../suites/replay/privacyInput/template.html | 1 + .../suites/replay/privacyInput/test.ts | 18 ++++++++ packages/replay-canvas/package.json | 2 +- packages/replay/package.json | 4 +- yarn.lock | 42 +++++++++---------- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 847db586afb0..fd5c3b90c040 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -46,7 +46,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.40.1", - "@sentry-internal/rrweb": "2.10.0", + "@sentry-internal/rrweb": "2.11.0", "@sentry/browser": "7.99.0", "@sentry/tracing": "7.99.0", "axios": "1.6.0", diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInput/template.html b/dev-packages/browser-integration-tests/suites/replay/privacyInput/template.html index fea3e1e29047..a5020bc956c1 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInput/template.html +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInput/template.html @@ -11,6 +11,7 @@ + diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts b/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts index 3b76e5622225..f2c506f90132 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts @@ -126,6 +126,18 @@ sentryTest( // This one should not have any input mutations return inputMutationSegmentIds.length === 2 && inputMutationSegmentIds[1] < event.segment_id; }); + const reqPromise4 = waitForReplayRequest(page, (event, res) => { + const check = + inputMutationSegmentIds.length === 2 && + inputMutationSegmentIds[1] < event.segment_id && + getIncrementalRecordingSnapshots(res).some(isInputMutation); + + if (check) { + inputMutationSegmentIds.push(event.segment_id); + } + + return check; + }); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ @@ -160,5 +172,11 @@ sentryTest( await forceFlushReplay(); const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation); expect(snapshots3.length).toBe(0); + + await page.locator('#should-still-be-masked').fill(text); + await forceFlushReplay(); + const snapshots4 = getIncrementalRecordingSnapshots(await reqPromise4).filter(isInputMutation); + const lastSnapshot4 = snapshots4[snapshots4.length - 1]; + expect(lastSnapshot4.data.text).toBe('*'.repeat(text.length)); }, ); diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 57abe8b43e64..12eb2aaa5017 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -56,7 +56,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/rrweb": "2.10.0" + "@sentry-internal/rrweb": "2.11.0" }, "dependencies": { "@sentry/core": "7.99.0", diff --git a/packages/replay/package.json b/packages/replay/package.json index 7c7eb6bd09d5..7447e60debb9 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -54,8 +54,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.99.0", - "@sentry-internal/rrweb": "2.10.0", - "@sentry-internal/rrweb-snapshot": "2.10.0", + "@sentry-internal/rrweb": "2.11.0", + "@sentry-internal/rrweb-snapshot": "2.11.0", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, diff --git a/yarn.lock b/yarn.lock index 1d97ef968f5c..f3e927b72635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5427,33 +5427,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.10.0.tgz#7f86667939a100bee2f82b6d459e275855ccc583" - integrity sha512-28G4W8BCdqI8GsO1SYkCBIwuizLwHrg8gE4u77v0zKpiaeIyZjYJ0QqhA/gMrTHLqrfI+FAwGXchnamjci45BA== +"@sentry-internal/rrdom@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.11.0.tgz#f7c8f54705ad84ece0e97e53f12e87c687749b32" + integrity sha512-BZnkTrbLm9Y3R70W1+8TnImys0RbKsgyB70WQoFdUervGvPw1kLcWJOJrPcDWgVe7nlbG+bEWb6iQrvLqldycw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.10.0" + "@sentry-internal/rrweb-snapshot" "2.11.0" -"@sentry-internal/rrweb-snapshot@2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.10.0.tgz#fa894fad3110fa8b912e41eb328bd956581c0ac0" - integrity sha512-/bqbmCzEn8o/hki9Jrng6xIkjczYlPHTEv+C/NDT7Q8A7WJ9KqIpCkljqyoNrD2o9OtwFuPAVgKyIPRkZF9ZfA== +"@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-types@2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.10.0.tgz#d9da0362c31c4e96b8649bbc9ab8bb380051caf3" - integrity sha512-nnwRrH0O8J+OsOEK3LeVruTv6JovZWEFywdacyfNt2LK7XTCG8182lU6bzPK3Ganb9ps2eOkJqOTRMYUZ1TrMA== +"@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" + integrity sha512-foCf9DGfN5ffzwykEtIXsV1P5d+XLDVGaQUnKF5ecGn+g5JzKTe/rPC92rL8/gEy2unL5sCTvlYL3DQvUFM4dA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.10.0" + "@sentry-internal/rrweb-snapshot" "2.11.0" -"@sentry-internal/rrweb@2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.10.0.tgz#a101f08f4b5de70145dbbdf70e7d2a0ac4d0d83e" - integrity sha512-S2xC0xxliCCgfowFImqIK6i9dfaEuTsLrzYkPxxX54OjqjrTsJw41aGxGfYPh+PP6nWMiURuOM5jRZrbvxoH4A== +"@sentry-internal/rrweb@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.11.0.tgz#be8e8dfff2acf64d418b625d35a20fdcd7daeb96" + integrity sha512-QuEqpKmRDb0xQe9fhJ3j/JHO6uxFMWBowADJBA4rvVU5HbExIg9gor1tZ0b3gDuChXnnx7pxFj9/QXZjQQ75zg== dependencies: - "@sentry-internal/rrdom" "2.10.0" - "@sentry-internal/rrweb-snapshot" "2.10.0" - "@sentry-internal/rrweb-types" "2.10.0" + "@sentry-internal/rrdom" "2.11.0" + "@sentry-internal/rrweb-snapshot" "2.11.0" + "@sentry-internal/rrweb-types" "2.11.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From cc0fcb8b3d98807bab4dbd75a8821d3a5eb1d931 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 2 Feb 2024 17:00:13 +0100 Subject: [PATCH 40/68] test(e2e): Add Vue 3 E2E tests (#10476) This PR adds e2e tests for a Vue 3 app using `@sentry/vue` Specifically, we test - Catching an error - Pageload transaction - Navigation transaction - Preferring route name over route id --- .github/workflows/build.yml | 3 +- biome.json | 8 +- .../test-applications/vue-3/.gitignore | 30 +++ .../e2e-tests/test-applications/vue-3/.npmrc | 2 + .../test-applications/vue-3/README.md | 3 + .../test-applications/vue-3/env.d.ts | 1 + .../vue-3/event-proxy-server.ts | 253 ++++++++++++++++++ .../test-applications/vue-3/index.html | 13 + .../test-applications/vue-3/package.json | 42 +++ .../vue-3/playwright.config.ts | 77 ++++++ .../vue-3/public/favicon.ico | Bin 0 -> 4286 bytes .../test-applications/vue-3/src/App.vue | 84 ++++++ .../vue-3/src/assets/base.css | 86 ++++++ .../vue-3/src/assets/logo.svg | 1 + .../vue-3/src/assets/main.css | 35 +++ .../test-applications/vue-3/src/main.ts | 25 ++ .../vue-3/src/router/index.ts | 23 ++ .../vue-3/src/views/AboutView.vue | 3 + .../vue-3/src/views/HomeView.vue | 14 + .../vue-3/src/views/UserIdView.vue | 3 + .../vue-3/start-event-proxy.ts | 6 + .../vue-3/tests/errors.test.ts | 29 ++ .../vue-3/tests/performance.test.ts | 102 +++++++ .../test-applications/vue-3/tsconfig.app.json | 14 + .../test-applications/vue-3/tsconfig.json | 11 + .../vue-3/tsconfig.node.json | 13 + .../vue-3/tsconfig.proxy.json | 11 + .../test-applications/vue-3/vite.config.ts | 16 ++ 28 files changed, 905 insertions(+), 3 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/README.md create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/index.html create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/package.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/App.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/assets/base.css create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/assets/logo.svg create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/assets/main.css create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/AboutView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/HomeView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/tsconfig.node.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/tsconfig.proxy.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/vite.config.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74ccf23dc669..c87ef1782865 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1031,7 +1031,8 @@ jobs: 'node-experimental-fastify-app', 'node-hapi-app', 'node-exports-test-app', - 'node-profiling' + 'node-profiling', + 'vue-3' ] build-command: - false diff --git a/biome.json b/biome.json index c18c0720b6d1..ccb69e4746db 100644 --- a/biome.json +++ b/biome.json @@ -41,7 +41,9 @@ ".next/**", ".svelte-kit/**", ".angular/**", - "angular.json" + "angular.json", + "ember/instance-initializers/**", + "ember/types.d.ts" ] }, "files": { @@ -65,7 +67,9 @@ ".svelte-kit/**", ".angular/**", "angular.json", - "**/profiling-node/lib/**" + "**/profiling-node/lib/**", + "ember/instance-initializers/**", + "ember/types.d.ts" ] }, "javascript": { diff --git a/dev-packages/e2e-tests/test-applications/vue-3/.gitignore b/dev-packages/e2e-tests/test-applications/vue-3/.gitignore new file mode 100644 index 000000000000..8ee54e8d343e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/dev-packages/e2e-tests/test-applications/vue-3/.npmrc b/dev-packages/e2e-tests/test-applications/vue-3/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/.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/vue-3/README.md b/dev-packages/e2e-tests/test-applications/vue-3/README.md new file mode 100644 index 000000000000..6af7bb60b866 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/README.md @@ -0,0 +1,3 @@ +# Vue 3 E2E Test App + +E2E test app for Vue 3 and `@sentry/vue`. diff --git a/dev-packages/e2e-tests/test-applications/vue-3/env.d.ts b/dev-packages/e2e-tests/test-applications/vue-3/env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/env.d.ts @@ -0,0 +1 @@ +/// 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 new file mode 100644 index 000000000000..4c2df32399f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/event-proxy-server.ts @@ -0,0 +1,253 @@ +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, SerializedEvent } 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, new TextEncoder(), new TextDecoder()), + 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: SerializedEvent) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: SerializedEvent) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); + 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/index.html b/dev-packages/e2e-tests/test-applications/vue-3/index.html new file mode 100644 index 000000000000..a888544898a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json new file mode 100644 index 000000000000..1fa4cbcf3882 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -0,0 +1,42 @@ +{ + "name": "vue-3-tmp", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "clean": "npx rimraf node_modules,pnpm-lock.yaml,dist", + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force", + "test": "playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "playwright test" + }, + "dependencies": { + "@sentry/vue": "latest || *", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@playwright/test": "^1.41.1", + "@sentry/types": "^7.99.0", + "@sentry/utils": "^7.99.0", + "@tsconfig/node20": "^20.1.2", + "@types/node": "^20.11.10", + "@vitejs/plugin-vue": "^5.0.3", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vue/tsconfig": "^0.5.1", + "http-server": "^14.1.1", + "npm-run-all2": "^6.1.1", + "ts-node": "10.9.1", + "typescript": "~5.3.0", + "vite": "^5.0.11", + "vue-tsc": "^1.8.27", + "wait-port": "1.0.4" + }, + "volta": { + "extends": "../../package.json" + } +} 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 new file mode 100644 index 000000000000..16dd640e58ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/playwright.config.ts @@ -0,0 +1,77 @@ +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 testEnv = process.env['TEST_ENV'] || 'production'; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const vuePort = 4173; +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: 10000, + }, + fullyParallel: false, + workers: 1, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* `next dev` is incredibly buggy with the app dir */ + retries: testEnv === 'development' ? 3 : 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:${vuePort}`, + + /* 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'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script --project tsconfig.proxy.json start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: + testEnv === 'development' + ? `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}` + : `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${vuePort}`, + port: vuePort, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/vue-3/public/favicon.ico b/dev-packages/e2e-tests/test-applications/vue-3/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/App.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/App.vue new file mode 100644 index 000000000000..08c38cecfda9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/App.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/assets/base.css b/dev-packages/e2e-tests/test-applications/vue-3/src/assets/base.css new file mode 100644 index 000000000000..8816868a41b6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/assets/logo.svg b/dev-packages/e2e-tests/test-applications/vue-3/src/assets/logo.svg new file mode 100644 index 000000000000..7565660356e5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/assets/main.css b/dev-packages/e2e-tests/test-applications/vue-3/src/assets/main.css new file mode 100644 index 000000000000..36fb845b5232 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts new file mode 100644 index 000000000000..503a9e44d14f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts @@ -0,0 +1,25 @@ +import './assets/main.css'; + +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router'; + +import * as Sentry from '@sentry/vue'; + +const app = createApp(App); + +Sentry.init({ + app, + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + tracesSampleRate: 1.0, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.vueRouterInstrumentation(router), + }), + ], + tunnel: `http://localhost:3031/`, // proxy server + debug: true, +}); + +app.use(router); +app.mount('#app'); 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 new file mode 100644 index 000000000000..a17208711eff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import HomeView from '../views/HomeView.vue'; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + component: HomeView, + }, + { + path: '/about', + name: 'AboutView', + component: () => import('../views/AboutView.vue'), + }, + { + path: '/users/:id', + component: () => import('../views/UserIdView.vue'), + }, + ], +}); + +export default router; diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/AboutView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/AboutView.vue new file mode 100644 index 000000000000..8c706352120a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/AboutView.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/HomeView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/HomeView.vue new file mode 100644 index 000000000000..92b38c308a6d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/HomeView.vue @@ -0,0 +1,14 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdView.vue new file mode 100644 index 000000000000..a6c973ef6e35 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdView.vue @@ -0,0 +1,3 @@ + 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 new file mode 100644 index 000000000000..6435984ad069 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'vue-3', +}); 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 new file mode 100644 index 000000000000..508fe738bbc5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('vue-3', async errorEvent => { + return !errorEvent?.transaction; + }); + + await page.goto(`/`); + + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'This is a Vue test error', + mechanism: { + type: 'generic', + handled: false, + }, + }, + ], + }, + }); +}); 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 new file mode 100644 index 000000000000..732ec98a54f4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts @@ -0,0 +1,102 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/users/456`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + params: { + id: '456', + }, + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/users/:id', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('vue-3', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('vue-3', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + data: { + params: { + id: '123', + }, + 'sentry.source': 'route', + 'sentry.origin': 'auto.navigation.vue', + 'sentry.op': 'navigation', + }, + op: 'navigation', + origin: 'auto.navigation.vue', + }, + }, + transaction: '/users/:id', + transaction_info: { + // So this is weird. The source is set to custom although the route doesn't have a name. + // This also only happens during a navigation. A pageload will set the source as 'route'. + // TODO: Figure out what's going on here. + source: 'custom', + }, + }); +}); + +test('sends a pageload transaction with a route name as transaction name if available', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/about`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'custom', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: 'AboutView', + transaction_info: { + source: 'custom', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.app.json new file mode 100644 index 000000000000..e14c754d3ae5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.json b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.json new file mode 100644 index 000000000000..78f134a16dca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ], +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.node.json new file mode 100644 index 000000000000..2c669eeb8e8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.proxy.json b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.proxy.json new file mode 100644 index 000000000000..7ccdde196a3b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/tsconfig.proxy.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ES2022", + "module": "ES2022", + }, + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node", + } +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3/vite.config.ts b/dev-packages/e2e-tests/test-applications/vue-3/vite.config.ts new file mode 100644 index 000000000000..72a15caeae52 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/vite.config.ts @@ -0,0 +1,16 @@ +import { URL, fileURLToPath } from 'node:url'; + +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), vueJsx()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + envPrefix: 'PUBLIC_', +}); From cfb1b60401a294c6b25ed967dccb4f1d338e8529 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 2 Feb 2024 17:43:50 +0100 Subject: [PATCH 41/68] feat(nextjs): Add `browserTracingIntegration` (#10397) --- .../nextjs-14/playwright.config.ts | 3 + .../nextjs-app-dir/playwright.config.ts | 3 + .../src/client/browserTracingIntegration.ts | 74 ++++++++- packages/nextjs/src/client/index.ts | 16 +- .../appRouterRoutingInstrumentation.ts | 23 ++- .../routing/nextRoutingInstrumentation.ts | 25 ++- .../pagesRouterRoutingInstrumentation.ts | 17 ++- packages/nextjs/test/clientSdk.test.ts | 144 +++++++++--------- .../appRouterInstrumentation.test.ts | 49 +++++- .../pagesRouterInstrumentation.test.ts | 48 +++++- 10 files changed, 297 insertions(+), 105 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts index ab3c40a21471..d855e4918ce5 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts @@ -1,3 +1,4 @@ +import os from 'os'; import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; @@ -31,6 +32,8 @@ const config: PlaywrightTestConfig = { }, /* Run tests in files in parallel */ fullyParallel: true, + /* Defaults to half the number of CPUs. The tests are not really CPU-bound but rather I/O-bound with all the polling we do so we increase the concurrency to the CPU count. */ + workers: os.cpus().length, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* `next dev` is incredibly buggy with the app dir */ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts index ab3c40a21471..599afc629b87 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts @@ -1,3 +1,4 @@ +import os from 'os'; import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; @@ -29,6 +30,8 @@ const config: PlaywrightTestConfig = { */ timeout: 10000, }, + /* Defaults to half the number of CPUs. The tests are not really CPU-bound but rather I/O-bound with all the polling we do so we increase the concurrency to the CPU count. */ + workers: os.cpus().length, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ diff --git a/packages/nextjs/src/client/browserTracingIntegration.ts b/packages/nextjs/src/client/browserTracingIntegration.ts index c3eb18887301..bf62725e105b 100644 --- a/packages/nextjs/src/client/browserTracingIntegration.ts +++ b/packages/nextjs/src/client/browserTracingIntegration.ts @@ -1,8 +1,17 @@ -import { BrowserTracing as OriginalBrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/react'; +import { + BrowserTracing as OriginalBrowserTracing, + browserTracingIntegration as originalBrowserTracingIntegration, + defaultRequestInstrumentationOptions, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, +} from '@sentry/react'; +import type { Integration, StartSpanOptions } from '@sentry/types'; import { nextRouterInstrumentation } from '../index.client'; /** * A custom BrowserTracing integration for Next.js. + * + * @deprecated Use `browserTracingIntegration` instead. */ export class BrowserTracing extends OriginalBrowserTracing { public constructor(options?: ConstructorParameters[0]) { @@ -19,8 +28,71 @@ export class BrowserTracing extends OriginalBrowserTracing { ] : // eslint-disable-next-line deprecation/deprecation [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], + // eslint-disable-next-line deprecation/deprecation routingInstrumentation: nextRouterInstrumentation, ...options, }); } } + +/** + * A custom BrowserTracing integration for Next.js. + */ +export function browserTracingIntegration( + options?: Parameters[0], +): Integration { + const browserTracingIntegrationInstance = originalBrowserTracingIntegration({ + // eslint-disable-next-line deprecation/deprecation + tracingOrigins: + process.env.NODE_ENV === 'development' + ? [ + // Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server + // has cors and it doesn't like extra headers when it's accessed from a different URL. + // TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764) + /^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/, + /^\/(?!\/)/, + ] + : // eslint-disable-next-line deprecation/deprecation + [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], + ...options, + instrumentNavigation: false, + instrumentPageLoad: false, + }); + + return { + ...browserTracingIntegrationInstance, + afterAllSetup(client) { + const startPageloadCallback = (startSpanOptions: StartSpanOptions): void => { + startBrowserTracingPageLoadSpan(client, startSpanOptions); + }; + + const startNavigationCallback = (startSpanOptions: StartSpanOptions): void => { + startBrowserTracingNavigationSpan(client, startSpanOptions); + }; + + // We need to run the navigation span instrumentation before the `afterAllSetup` hook on the normal browser + // tracing integration because we need to ensure the order of execution is as follows: + // Instrumentation to start span on RSC fetch request runs -> Instrumentation to put tracing headers from active span on fetch runs + // If it were the other way around, the RSC fetch request would not receive the tracing headers from the navigation transaction. + // eslint-disable-next-line deprecation/deprecation + nextRouterInstrumentation( + () => undefined, + false, + options?.instrumentNavigation, + startPageloadCallback, + startNavigationCallback, + ); + + browserTracingIntegrationInstance.afterAllSetup(client); + + // eslint-disable-next-line deprecation/deprecation + nextRouterInstrumentation( + () => undefined, + options?.instrumentPageLoad, + false, + startPageloadCallback, + startNavigationCallback, + ); + }, + }; +} diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index a1c20937f578..e0d22445a3a1 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,5 +1,5 @@ import { applySdkMetadata, hasTracingEnabled } from '@sentry/core'; -import type { BrowserOptions, browserTracingIntegration } from '@sentry/react'; +import type { BrowserOptions } from '@sentry/react'; import { Integrations as OriginalIntegrations, getCurrentScope, @@ -10,11 +10,13 @@ import type { EventProcessor, Integration } from '@sentry/types'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; +import { browserTracingIntegration } from './browserTracingIntegration'; import { BrowserTracing } from './browserTracingIntegration'; import { rewriteFramesIntegration } from './rewriteFramesIntegration'; import { applyTunnelRouteOption } from './tunnelRoute'; export * from '@sentry/react'; +// eslint-disable-next-line deprecation/deprecation export { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation'; export { captureUnderscoreErrorException } from '../common/_error'; @@ -35,6 +37,7 @@ export const Integrations = { // // import { BrowserTracing } from '@sentry/nextjs'; // const instance = new BrowserTracing(); +// eslint-disable-next-line deprecation/deprecation export { BrowserTracing, rewriteFramesIntegration }; // Treeshakable guard to remove all code related to tracing @@ -68,7 +71,7 @@ export function init(options: BrowserOptions): void { } // TODO v8: Remove this again -// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/sveltekit` :( +// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/nextjs` :( function fixBrowserTracingIntegration(options: BrowserOptions): void { const { integrations } = options; if (!integrations) { @@ -89,6 +92,7 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void { function isNewBrowserTracingIntegration( integration: Integration, ): integration is Integration & { options?: Parameters[0] } { + // eslint-disable-next-line deprecation/deprecation return !!integration.afterAllSetup && !!(integration as BrowserTracing).options; } @@ -102,17 +106,21 @@ function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Inte // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one if (isNewBrowserTracingIntegration(browserTracing)) { const { options } = browserTracing; + // eslint-disable-next-line deprecation/deprecation integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); } // If BrowserTracing was added, but it is not our forked version, // replace it with our forked version with the same options + // eslint-disable-next-line deprecation/deprecation if (!(browserTracing instanceof BrowserTracing)) { + // eslint-disable-next-line deprecation/deprecation const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options; // This option is overwritten by the custom integration delete options.routingInstrumentation; // eslint-disable-next-line deprecation/deprecation delete options.tracingOrigins; + // eslint-disable-next-line deprecation/deprecation integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); } @@ -126,7 +134,7 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] { // will get treeshaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { if (hasTracingEnabled(options)) { - customDefaultIntegrations.push(new BrowserTracing()); + customDefaultIntegrations.push(browserTracingIntegration()); } } @@ -140,4 +148,6 @@ export function withSentryConfig(exportedUserNextConfig: T): T { return exportedUserNextConfig; } +export { browserTracingIntegration } from './browserTracingIntegration'; + export * from '../common'; diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 3083013e084a..25ec697a2161 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -1,30 +1,34 @@ import { WINDOW } from '@sentry/react'; -import type { Primitive, Transaction, TransactionContext } from '@sentry/types'; +import type { Primitive, Span, StartSpanOptions, Transaction, TransactionContext } from '@sentry/types'; import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils'; type StartTransactionCb = (context: TransactionContext) => Transaction | undefined; +type StartSpanCb = (context: StartSpanOptions) => void; const DEFAULT_TAGS = { 'routing.instrumentation': 'next-app-router', } as const; /** - * Instruments the Next.js Clientside App Router. + * Instruments the Next.js Client App Router. */ +// TODO(v8): Clean this function up by splitting into pageload and navigation instrumentation respectively. Also remove startTransactionCb in the process. export function appRouterInstrumentation( startTransactionCb: StartTransactionCb, startTransactionOnPageLoad: boolean = true, startTransactionOnLocationChange: boolean = true, + startPageloadSpanCallback: StartSpanCb, + startNavigationSpanCallback: StartSpanCb, ): void { // We keep track of the active transaction so we can finish it when we start a navigation transaction. - let activeTransaction: Transaction | undefined = undefined; + let activeTransaction: Span | undefined = undefined; // We keep track of the previous location name so we can set the `from` field on navigation transactions. // This is either a route or a pathname. let prevLocationName = WINDOW.location.pathname; if (startTransactionOnPageLoad) { - activeTransaction = startTransactionCb({ + const transactionContext = { name: prevLocationName, op: 'pageload', origin: 'auto.pageload.nextjs.app_router_instrumentation', @@ -32,7 +36,9 @@ export function appRouterInstrumentation( // pageload should always start at timeOrigin (and needs to be in s, not ms) startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, metadata: { source: 'url' }, - }); + } as const; + activeTransaction = startTransactionCb(transactionContext); + startPageloadSpanCallback(transactionContext); } if (startTransactionOnLocationChange) { @@ -66,13 +72,16 @@ export function appRouterInstrumentation( activeTransaction.end(); } - startTransactionCb({ + const transactionContext = { name: transactionName, op: 'navigation', origin: 'auto.navigation.nextjs.app_router_instrumentation', tags, metadata: { source: 'url' }, - }); + } as const; + + startTransactionCb(transactionContext); + startNavigationSpanCallback(transactionContext); }); } } diff --git a/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts index 3010faad4183..4706fb8a32f2 100644 --- a/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts @@ -1,23 +1,40 @@ import { WINDOW } from '@sentry/react'; -import type { Transaction, TransactionContext } from '@sentry/types'; +import type { StartSpanOptions, Transaction, TransactionContext } from '@sentry/types'; import { appRouterInstrumentation } from './appRouterRoutingInstrumentation'; import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation'; type StartTransactionCb = (context: TransactionContext) => Transaction | undefined; +type StartSpanCb = (context: StartSpanOptions) => void; /** - * Instruments the Next.js Clientside Router. + * Instruments the Next.js Client Router. + * + * @deprecated Use `browserTracingIntegration()` as exported from `@sentry/nextjs` instead. */ export function nextRouterInstrumentation( startTransactionCb: StartTransactionCb, startTransactionOnPageLoad: boolean = true, startTransactionOnLocationChange: boolean = true, + startPageloadSpanCallback?: StartSpanCb, + startNavigationSpanCallback?: StartSpanCb, ): void { const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__'); if (isAppRouter) { - appRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange); + appRouterInstrumentation( + startTransactionCb, + startTransactionOnPageLoad, + startTransactionOnLocationChange, + startPageloadSpanCallback || (() => undefined), + startNavigationSpanCallback || (() => undefined), + ); } else { - pagesRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange); + pagesRouterInstrumentation( + startTransactionCb, + startTransactionOnPageLoad, + startTransactionOnLocationChange, + startPageloadSpanCallback || (() => undefined), + startNavigationSpanCallback || (() => undefined), + ); } } diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index e360e51df56b..c3f466a566ea 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -1,7 +1,7 @@ import type { ParsedUrlQuery } from 'querystring'; import { getClient, getCurrentScope } from '@sentry/core'; import { WINDOW } from '@sentry/react'; -import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Primitive, StartSpanOptions, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { browserPerformanceTimeOrigin, logger, @@ -20,6 +20,7 @@ const globalObject = WINDOW as typeof WINDOW & { }; type StartTransactionCb = (context: TransactionContext) => Transaction | undefined; +type StartSpanCb = (context: StartSpanOptions) => void; /** * Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app. @@ -117,6 +118,8 @@ export function pagesRouterInstrumentation( startTransactionCb: StartTransactionCb, startTransactionOnPageLoad: boolean = true, startTransactionOnLocationChange: boolean = true, + startPageloadSpanCallback: StartSpanCb, + startNavigationSpanCallback: StartSpanCb, ): void { const { route, params, sentryTrace, baggage } = extractNextDataTagInformation(); // eslint-disable-next-line deprecation/deprecation @@ -130,7 +133,7 @@ export function pagesRouterInstrumentation( if (startTransactionOnPageLoad) { const source = route ? 'route' : 'url'; - activeTransaction = startTransactionCb({ + const transactionContext = { name: prevLocationName, op: 'pageload', origin: 'auto.pageload.nextjs.pages_router_instrumentation', @@ -143,7 +146,9 @@ export function pagesRouterInstrumentation( source, dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, - }); + } as const; + activeTransaction = startTransactionCb(transactionContext); + startPageloadSpanCallback(transactionContext); } if (startTransactionOnLocationChange) { @@ -173,13 +178,15 @@ export function pagesRouterInstrumentation( activeTransaction.end(); } - const navigationTransaction = startTransactionCb({ + const transactionContext = { name: transactionName, op: 'navigation', origin: 'auto.navigation.nextjs.pages_router_instrumentation', tags, metadata: { source: transactionSource }, - }); + } as const; + const navigationTransaction = startTransactionCb(transactionContext); + startNavigationSpanCallback(transactionContext); if (navigationTransaction) { // In addition to the navigation transaction we're also starting a span to mark Next.js's `routeChangeStart` diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index f4ec99c3cc71..0ce7733dc137 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -117,111 +117,107 @@ describe('Client init()', () => { expect(installedBreadcrumbsIntegration).toBeDefined(); }); - describe('`BrowserTracing` integration', () => { - it('adds `BrowserTracing` integration if `tracesSampleRate` is set', () => { - init({ - dsn: TEST_DSN, - tracesSampleRate: 1.0, - }); + it('forces correct router instrumentation if user provides `BrowserTracing` in an array', () => { + init({ + dsn: TEST_DSN, + tracesSampleRate: 1.0, + // eslint-disable-next-line deprecation/deprecation + integrations: [new BrowserTracing({ finalTimeout: 10 })], + }); - const client = getClient()!; - const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + const client = getClient()!; + // eslint-disable-next-line deprecation/deprecation + const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + + expect(browserTracingIntegration).toBeDefined(); + expect(browserTracingIntegration?.options).toEqual( + expect.objectContaining({ + // eslint-disable-next-line deprecation/deprecation + routingInstrumentation: nextRouterInstrumentation, + // This proves it's still the user's copy + finalTimeout: 10, + }), + ); + }); - expect(browserTracingIntegration).toBeDefined(); - expect(browserTracingIntegration?.options).toEqual( - expect.objectContaining({ - routingInstrumentation: nextRouterInstrumentation, - }), - ); + it('forces correct router instrumentation if user provides `browserTracingIntegration`', () => { + init({ + dsn: TEST_DSN, + integrations: [browserTracingIntegration({ finalTimeout: 10 })], + enableTracing: true, }); - it('adds `BrowserTracing` integration if `tracesSampler` is set', () => { - init({ - dsn: TEST_DSN, - tracesSampler: () => true, - }); - - const client = getClient()!; - const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + const client = getClient()!; + // eslint-disable-next-line deprecation/deprecation + const integration = client.getIntegrationByName('BrowserTracing'); + + expect(integration).toBeDefined(); + expect(integration?.options).toEqual( + expect.objectContaining({ + // eslint-disable-next-line deprecation/deprecation + routingInstrumentation: nextRouterInstrumentation, + // This proves it's still the user's copy + finalTimeout: 10, + }), + ); + }); - expect(browserTracingIntegration).toBeDefined(); - expect(browserTracingIntegration?.options).toEqual( - expect.objectContaining({ - routingInstrumentation: nextRouterInstrumentation, - }), - ); + it('forces correct router instrumentation if user provides `BrowserTracing` in a function', () => { + init({ + dsn: TEST_DSN, + tracesSampleRate: 1.0, + // eslint-disable-next-line deprecation/deprecation + integrations: defaults => [...defaults, new BrowserTracing({ startTransactionOnLocationChange: false })], }); - it('does not add `BrowserTracing` integration if tracing not enabled in SDK', () => { - init({ - dsn: TEST_DSN, - }); + const client = getClient()!; - const client = getClient()!; - const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + // eslint-disable-next-line deprecation/deprecation + const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); - expect(browserTracingIntegration).toBeUndefined(); - }); + expect(browserTracingIntegration).toBeDefined(); + expect(browserTracingIntegration?.options).toEqual( + expect.objectContaining({ + // eslint-disable-next-line deprecation/deprecation + routingInstrumentation: nextRouterInstrumentation, + // This proves it's still the user's copy + startTransactionOnLocationChange: false, + }), + ); + }); - it('forces correct router instrumentation if user provides `BrowserTracing` in an array', () => { + describe('browserTracingIntegration()', () => { + it('adds `browserTracingIntegration()` integration if `tracesSampleRate` is set', () => { init({ dsn: TEST_DSN, tracesSampleRate: 1.0, - integrations: [new BrowserTracing({ finalTimeout: 10 })], }); const client = getClient()!; - const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); - - expect(browserTracingIntegration).toBeDefined(); - expect(browserTracingIntegration?.options).toEqual( - expect.objectContaining({ - routingInstrumentation: nextRouterInstrumentation, - // This proves it's still the user's copy - finalTimeout: 10, - }), - ); + const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + expect(browserTracingIntegration?.name).toBe('BrowserTracing'); }); - it('forces correct router instrumentation if user provides `browserTracingIntegration`', () => { + it('adds `browserTracingIntegration()` integration if `tracesSampler` is set', () => { init({ dsn: TEST_DSN, - integrations: [browserTracingIntegration({ finalTimeout: 10 })], - enableTracing: true, + tracesSampler: () => true, }); const client = getClient()!; - const integration = client.getIntegrationByName('BrowserTracing'); - - expect(integration).toBeDefined(); - expect(integration?.options).toEqual( - expect.objectContaining({ - routingInstrumentation: nextRouterInstrumentation, - // This proves it's still the user's copy - finalTimeout: 10, - }), - ); + const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + expect(browserTracingIntegration?.name).toBe('BrowserTracing'); }); - it('forces correct router instrumentation if user provides `BrowserTracing` in a function', () => { + it('does not add `browserTracingIntegration()` integration if tracing not enabled in SDK', () => { init({ dsn: TEST_DSN, - tracesSampleRate: 1.0, - integrations: defaults => [...defaults, new BrowserTracing({ startTransactionOnLocationChange: false })], }); const client = getClient()!; - const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); - - expect(browserTracingIntegration).toBeDefined(); - expect(browserTracingIntegration?.options).toEqual( - expect.objectContaining({ - routingInstrumentation: nextRouterInstrumentation, - // This proves it's still the user's copy - startTransactionOnLocationChange: false, - }), - ); + const browserTracingIntegration = client.getIntegrationByName('BrowserTracing'); + expect(browserTracingIntegration).toBeUndefined(); }); }); }); diff --git a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts index 3337b99ab9a9..34a6b31fc60f 100644 --- a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts @@ -30,7 +30,10 @@ describe('appRouterInstrumentation', () => { it('should create a pageload transactions with the current location name', () => { setUpPage('https://example.com/some/page?someParam=foobar'); const startTransactionCallbackFn = jest.fn(); - appRouterInstrumentation(startTransactionCallbackFn, true, false); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); + + appRouterInstrumentation(startTransactionCallbackFn, true, false, mockStartPageloadSpan, mockStartNavigationSpan); expect(startTransactionCallbackFn).toHaveBeenCalledWith( expect.objectContaining({ name: '/some/page', @@ -42,12 +45,26 @@ describe('appRouterInstrumentation', () => { metadata: { source: 'url' }, }), ); + expect(mockStartPageloadSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: '/some/page', + op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', + tags: { + 'routing.instrumentation': 'next-app-router', + }, + metadata: { source: 'url' }, + }), + ); }); it('should not create a pageload transaction when `startTransactionOnPageLoad` is false', () => { setUpPage('https://example.com/some/page?someParam=foobar'); const startTransactionCallbackFn = jest.fn(); - appRouterInstrumentation(startTransactionCallbackFn, false, false); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); + + appRouterInstrumentation(startTransactionCallbackFn, false, false, mockStartPageloadSpan, mockStartNavigationSpan); expect(startTransactionCallbackFn).not.toHaveBeenCalled(); }); @@ -60,7 +77,10 @@ describe('appRouterInstrumentation', () => { }); const startTransactionCallbackFn = jest.fn(); - appRouterInstrumentation(startTransactionCallbackFn, false, true); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); + + appRouterInstrumentation(startTransactionCallbackFn, false, true, mockStartPageloadSpan, mockStartNavigationSpan); fetchInstrumentationHandlerCallback!({ args: [ @@ -85,6 +105,16 @@ describe('appRouterInstrumentation', () => { 'routing.instrumentation': 'next-app-router', }, }); + expect(mockStartNavigationSpan).toHaveBeenCalledWith({ + name: '/some/server/component/page', + op: 'navigation', + origin: 'auto.navigation.nextjs.app_router_instrumentation', + metadata: { source: 'url' }, + tags: { + from: '/some/page', + 'routing.instrumentation': 'next-app-router', + }, + }); }); it.each([ @@ -133,7 +163,7 @@ describe('appRouterInstrumentation', () => { }, ], ])( - 'should not create naviagtion transactions for fetch requests that are not navigating RSC requests (%s)', + 'should not create navigation transactions for fetch requests that are not navigating RSC requests (%s)', (_, fetchCallbackData) => { setUpPage('https://example.com/some/page?someParam=foobar'); let fetchInstrumentationHandlerCallback: (arg: HandlerDataFetch) => void; @@ -143,9 +173,13 @@ describe('appRouterInstrumentation', () => { }); const startTransactionCallbackFn = jest.fn(); - appRouterInstrumentation(startTransactionCallbackFn, false, true); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); + + appRouterInstrumentation(startTransactionCallbackFn, false, true, mockStartPageloadSpan, mockStartNavigationSpan); fetchInstrumentationHandlerCallback!(fetchCallbackData); expect(startTransactionCallbackFn).not.toHaveBeenCalled(); + expect(mockStartNavigationSpan).not.toHaveBeenCalled(); }, ); @@ -153,9 +187,12 @@ describe('appRouterInstrumentation', () => { setUpPage('https://example.com/some/page?someParam=foobar'); const addFetchInstrumentationHandlerImpl = jest.fn(); const startTransactionCallbackFn = jest.fn(); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); addFetchInstrumentationHandlerSpy.mockImplementationOnce(addFetchInstrumentationHandlerImpl); - appRouterInstrumentation(startTransactionCallbackFn, false, false); + appRouterInstrumentation(startTransactionCallbackFn, false, false, mockStartPageloadSpan, mockStartNavigationSpan); expect(addFetchInstrumentationHandlerImpl).not.toHaveBeenCalled(); + expect(mockStartNavigationSpan).not.toHaveBeenCalled(); }); }); diff --git a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts index 592df911bde2..3e032c1f01d1 100644 --- a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts @@ -211,20 +211,41 @@ describe('pagesRouterInstrumentation', () => { 'creates a pageload transaction (#%#)', (url, route, query, props, hasNextData, expectedStartTransactionArgument) => { const mockStartTransaction = createMockStartTransaction(); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); + setUpNextPage({ url, route, query, props, hasNextData }); - pagesRouterInstrumentation(mockStartTransaction); + pagesRouterInstrumentation( + mockStartTransaction, + undefined, + undefined, + mockStartPageloadSpan, + mockStartNavigationSpan, + ); + expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith( expect.objectContaining(expectedStartTransactionArgument), ); + expect(mockStartPageloadSpan).toHaveBeenCalledWith(expect.objectContaining(expectedStartTransactionArgument)); }, ); it('does not create a pageload transaction if option not given', () => { const mockStartTransaction = createMockStartTransaction(); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); + setUpNextPage({ url: 'https://example.com/', route: '/', hasNextData: false }); - pagesRouterInstrumentation(mockStartTransaction, false); - expect(mockStartTransaction).toHaveBeenCalledTimes(0); + pagesRouterInstrumentation( + mockStartTransaction, + false, + undefined, + mockStartPageloadSpan, + mockStartNavigationSpan, + ); + expect(mockStartTransaction).not.toHaveBeenCalled(); + expect(mockStartPageloadSpan).not.toHaveBeenCalled(); }); }); @@ -252,6 +273,8 @@ describe('pagesRouterInstrumentation', () => { 'should create a parameterized transaction on route change (%s)', (targetLocation, expectedTransactionName, expectedTransactionSource) => { const mockStartTransaction = createMockStartTransaction(); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); setUpNextPage({ url: 'https://example.com/home', @@ -270,7 +293,7 @@ describe('pagesRouterInstrumentation', () => { ], }); - pagesRouterInstrumentation(mockStartTransaction, false, true); + pagesRouterInstrumentation(mockStartTransaction, false, true, mockStartPageloadSpan, mockStartNavigationSpan); Router.events.emit('routeChangeStart', targetLocation); @@ -287,6 +310,18 @@ describe('pagesRouterInstrumentation', () => { }), }), ); + expect(mockStartNavigationSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: expectedTransactionName, + op: 'navigation', + tags: expect.objectContaining({ + 'routing.instrumentation': 'next-pages-router', + }), + metadata: expect.objectContaining({ + source: expectedTransactionSource, + }), + }), + ); Router.events.emit('routeChangeComplete', targetLocation); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -298,6 +333,8 @@ describe('pagesRouterInstrumentation', () => { it('should not create transaction when navigation transactions are disabled', () => { const mockStartTransaction = createMockStartTransaction(); + const mockStartPageloadSpan = jest.fn(); + const mockStartNavigationSpan = jest.fn(); setUpNextPage({ url: 'https://example.com/home', @@ -306,11 +343,12 @@ describe('pagesRouterInstrumentation', () => { navigatableRoutes: ['/home', '/posts/[id]'], }); - pagesRouterInstrumentation(mockStartTransaction, false, false); + pagesRouterInstrumentation(mockStartTransaction, false, false, mockStartPageloadSpan, mockStartNavigationSpan); Router.events.emit('routeChangeStart', '/posts/42'); expect(mockStartTransaction).not.toHaveBeenCalled(); + expect(mockStartNavigationSpan).not.toHaveBeenCalled(); }); }); }); From 84baeb1847b0cd90af43ba8b29517b1b6cb24854 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 2 Feb 2024 17:56:35 +0100 Subject: [PATCH 42/68] fix(nextjs): Do not report redirects and notFound calls as errors in server actions (#10474) --- .../nextjs-app-dir/app/server-action/page.tsx | 29 +++++++++++++++---- .../nextjs-app-dir/tests/transactions.test.ts | 16 ++++++++++ .../common/withServerActionInstrumentation.ts | 15 +++++++++- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-action/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-action/page.tsx index 4137fafd9c3c..6784970d2aae 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-action/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-action/page.tsx @@ -1,5 +1,6 @@ import * as Sentry from '@sentry/nextjs'; import { headers } from 'next/headers'; +import { notFound } from 'next/navigation'; export default function ServerComponent() { async function myServerAction(formData: FormData) { @@ -14,11 +15,29 @@ export default function ServerComponent() { ); } + async function notFoundServerAction(formData: FormData) { + 'use server'; + return await Sentry.withServerActionInstrumentation( + 'notFoundServerAction', + { formData, headers: headers(), recordResponse: true }, + () => { + notFound(); + }, + ); + } + return ( - // @ts-ignore -
- - -
+ <> + {/* @ts-ignore */} +
+ + +
+ {/* @ts-ignore */} +
+ + +
+ ); } 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 3532c5c64746..6c99733381b6 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 @@ -140,6 +140,22 @@ test('Should send a transaction for instrumented server actions', async ({ page expect(Object.keys((await serverComponentTransactionPromise).request?.headers || {}).length).toBeGreaterThan(0); }); +test('Should set not_found status for server actions calling notFound()', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14'); + + const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'serverAction/notFoundServerAction'; + }); + + await page.goto('/server-action'); + await page.getByText('Run NotFound Action').click(); + + expect(await serverComponentTransactionPromise).toBeDefined(); + expect(await (await serverComponentTransactionPromise).contexts?.trace?.status).toBe('not_found'); +}); + test('Will not include spans in pageload transaction with faulty timestamps for slow loading pages', async ({ page, }) => { diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 0d0e6968a3b1..2fe1fd714b96 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -10,6 +10,7 @@ import { import { logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; +import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; import { flushQueue } from './utils/responseEnd'; @@ -101,7 +102,19 @@ async function withServerActionInstrumentationImplementation { const result = await handleCallbackErrors(callback, error => { - captureException(error, { mechanism: { handled: false } }); + if (isNotFoundNavigationError(error)) { + // We don't want to report "not-found"s + span?.setStatus('not_found'); + } else if (isRedirectNavigationError(error)) { + // Don't do anything for redirects + } else { + span?.setStatus('internal_error'); + captureException(error, { + mechanism: { + handled: false, + }, + }); + } }); if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) { From 004efa6c09a23cb415365ca42ce76c09994a86db Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 5 Feb 2024 10:43:18 +0100 Subject: [PATCH 43/68] build(ci): Properly skip node CI steps when not changed (#10487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One part of https://github.com/getsentry/sentry-javascript/issues/10486, actually our checks for this were not correct 😅 This is still only a partial fix as if we change anything in core etc. we'll still have the build overhead every time, but it's something! --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c87ef1782865..d509d27c070b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -510,6 +510,7 @@ jobs: job_node_unit_tests: name: Node (${{ matrix.node }}) Unit Tests + if: needs.job_get_metadata.outputs.changed_node == 'true' || github.event_name != 'pull_request' needs: [job_get_metadata, job_build] timeout-minutes: 10 runs-on: ubuntu-20.04 @@ -542,7 +543,7 @@ jobs: job_profiling_node_unit_tests: name: Node Profiling Unit Tests needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_node || needs.job_get_metadata.outputs.changed_profiling_node == 'true' || github.event_name != 'pull_request' + if: needs.job_get_metadata.outputs.changed_node =='true' || needs.job_get_metadata.outputs.changed_profiling_node == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -1217,7 +1218,7 @@ jobs: # if profiling or profiling node package had changed or if we are on a release branch. if: | (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release) || + (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') runs-on: ${{ matrix.os }} container: ${{ matrix.container }} From 4285a7e5ab5ee8878e236a1d05725f4d006925be Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 5 Feb 2024 10:50:18 +0100 Subject: [PATCH 44/68] build(ci): Explicitly add codecov token (#10485) This is necessary since v4 of the action. --- .github/workflows/build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d509d27c070b..1b5929258cb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -446,6 +446,8 @@ jobs: run: yarn test-ci-browser - name: Compute test coverage uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} job_bun_unit_tests: name: Bun Unit Tests @@ -474,6 +476,8 @@ jobs: yarn test-ci-bun - name: Compute test coverage uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} job_deno_unit_tests: name: Deno Unit Tests @@ -507,6 +511,8 @@ jobs: yarn test - name: Compute test coverage uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} job_node_unit_tests: name: Node (${{ matrix.node }}) Unit Tests @@ -539,6 +545,8 @@ jobs: yarn test-ci-node - name: Compute test coverage uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} job_profiling_node_unit_tests: name: Node Profiling Unit Tests From 028f4d5278872147a02fe02f30acd6ec6fe3a043 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 5 Feb 2024 13:28:58 +0100 Subject: [PATCH 45/68] fix(node): Use normal `require` call to import Undici (#10388) --- .../app/request-instrumentation/page.tsx | 2 +- packages/node/package.json | 2 +- .../node/src/integrations/undici/index.ts | 3 +- .../manual/webpack-async-context/npm-build.js | 5 + .../manual/webpack-async-context/package.json | 6 +- .../manual/webpack-async-context/yarn.lock | 2908 +++-------------- 6 files changed, 385 insertions(+), 2541 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx index 7a226868d1bd..a1092a7fa618 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx @@ -3,7 +3,7 @@ import http from 'http'; export const dynamic = 'force-dynamic'; export default async function Page() { - await fetch('http://example.com/'); + await fetch('http://example.com/', { cache: 'no-cache' }); await new Promise(resolve => { http.get('http://example.com/', () => { resolve(); diff --git a/packages/node/package.json b/packages/node/package.json index 689fcb3849aa..31ba67574c1c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -63,7 +63,7 @@ "test:express": "node test/manual/express-scope-separation/start.js", "test:jest": "jest", "test:release-health": "node test/manual/release-health/runner.js", - "test:webpack": "cd test/manual/webpack-async-context/ && yarn --silent && node npm-build.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" }, diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index d53533699104..a2616deb920b 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -15,7 +15,6 @@ import { import type { EventProcessor, Integration, IntegrationFn, IntegrationFnResult, Span } from '@sentry/types'; import { LRUMap, - dynamicRequire, dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, getSanitizedUrlString, @@ -127,7 +126,7 @@ export class Undici implements Integration { let ds: DiagnosticsChannel | undefined; try { // eslint-disable-next-line @typescript-eslint/no-var-requires - ds = dynamicRequire(module, 'diagnostics_channel') as DiagnosticsChannel; + ds = require('diagnostics_channel') as DiagnosticsChannel; } catch (e) { // no-op } diff --git a/packages/node/test/manual/webpack-async-context/npm-build.js b/packages/node/test/manual/webpack-async-context/npm-build.js index eac357b10f36..9d9c687981bb 100644 --- a/packages/node/test/manual/webpack-async-context/npm-build.js +++ b/packages/node/test/manual/webpack-async-context/npm-build.js @@ -7,6 +7,11 @@ if (Number(process.versions.node.split('.')[0]) >= 18) { process.exit(0); } +// Webpack test does not work in Node 8 and below. +if (Number(process.versions.node.split('.')[0]) <= 8) { + process.exit(0); +} + // biome-ignore format: Follow-up for prettier webpack( { diff --git a/packages/node/test/manual/webpack-async-context/package.json b/packages/node/test/manual/webpack-async-context/package.json index ff8f85afdafa..666406416c06 100644 --- a/packages/node/test/manual/webpack-async-context/package.json +++ b/packages/node/test/manual/webpack-async-context/package.json @@ -4,9 +4,9 @@ "main": "index.js", "license": "MIT", "dependencies": { - "webpack": "^4.42.1" + "webpack": "^5.90.0" }, - "devDependencies": { - "webpack-cli": "^3.3.11" + "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 index 5c67ad308da3..5ae121f60447 100644 --- a/packages/node/test/manual/webpack-async-context/yarn.lock +++ b/packages/node/test/manual/webpack-async-context/yarn.lock @@ -2,149 +2,198 @@ # yarn lockfile v1 -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== +"@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: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== +"@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== -"@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" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== +"@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== -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== +"@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: - "@webassemblyjs/wast-printer" "1.9.0" + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== +"@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== -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== +"@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: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" -"@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" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== +"@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: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" + "@types/eslint" "*" + "@types/estree" "*" -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== +"@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: - "@xtuc/ieee754" "^1.2.0" + "@types/estree" "*" + "@types/json-schema" "*" -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== - dependencies: +"@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/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== +"@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/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== +"@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.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@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" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" + "@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/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== +"@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: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" + "@xtuc/ieee754" "^1.2.0" -"@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" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== +"@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: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" "@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" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" +"@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": @@ -157,22 +206,22 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -acorn@^6.2.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== +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== -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== +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.1.0, ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== +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.1.0, ajv@^6.10.2: +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== @@ -182,343 +231,25 @@ ajv@^6.1.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-regex@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -asn1.js@^4.0.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" - integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== +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: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.4" - inherits "^2.0.4" - parse-asn1 "^5.1.6" - readable-stream "^3.6.2" - safe-buffer "^5.2.1" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" + 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== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -cacache@^12.0.2: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -chalk@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chokidar@^2.0.2: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +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" @@ -527,414 +258,63 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - 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== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +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== -create-ecdh@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" - integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== +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: - bn.js "^4.1.0" - elliptic "^6.0.0" + graceful-fs "^4.2.4" + tapable "^2.2.0" -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@6.0.5, cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -cyclist@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" - integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -elliptic@^6.0.0, elliptic@^6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" - integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -errno@^0.1.3, errno@~0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" +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== -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +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@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== +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.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== +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 "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -events@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" - integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= - dependencies: - homedir-polyfill "^1.0.1" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" +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== -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" +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" @@ -946,1328 +326,116 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -findup-sync@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stream@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob@^7.1.3, glob@^7.1.4: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-modules@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" +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.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: +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== -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - -import-local@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -infer-owner@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -interpret@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== - -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +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== -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +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== -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= +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: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" -json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +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== -json5@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - -loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - -loader-utils@^1.2.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -make-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mem@^4.0.0: +loader-runner@^4.2.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.0, memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mimic-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5.1, mkdirp@^0.5.3: - version "0.5.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" - integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== - dependencies: - minimist "^1.2.5" - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -ms@2.0.0: +merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -nan@^2.12.1: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -neo-async@^2.5.0, neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -p-limit@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" - integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== - dependencies: - p-try "^2.0.0" +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== -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== +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: - p-limit "^2.0.0" + mime-db "1.52.0" -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +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== -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - -parse-asn1@^5.0.0: - version "5.1.5" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" - integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== - dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-asn1@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" +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== -parse-passwd@^1.0.0: +picocolors@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -pbkdf2@^3.0.3: - version "3.0.17" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" - integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + 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== -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +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" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rimraf@^2.5.4, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.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== -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== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -semver@^5.5.0, semver@^5.6.0: - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= +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: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== +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: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" + randombytes "^2.1.0" -source-map-support@~0.5.12: +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== @@ -2275,249 +443,61 @@ source-map-support@~0.5.12: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +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== -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== +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: - extend-shallow "^3.0.0" + has-flag "^4.0.0" -ssri@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" - integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== - dependencies: - figgy-pudding "^3.5.1" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -supports-color@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -terser-webpack-plugin@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" - integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== - dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^2.1.2" - source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" - -terser@^4.1.2: - version "4.8.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" - integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== - dependencies: +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 "~0.6.1" - source-map-support "~0.5.12" - -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -timers-browserify@^2.0.4: - version "2.0.11" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" - integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== - dependencies: - setimmediate "^1.0.4" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.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== -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" +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== -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= +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: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + escalade "^3.1.1" + picocolors "^1.0.0" uri-js@^4.2.2: version "4.4.1" @@ -2526,185 +506,45 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -v8-compile-cache@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" - integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -watchpack@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" - integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== +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: - chokidar "^2.0.2" + glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" - neo-async "^2.5.0" - -webpack-cli@^3.3.11: - version "3.3.11" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" - integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== - dependencies: - chalk "2.4.2" - cross-spawn "6.0.5" - enhanced-resolve "4.1.0" - findup-sync "3.0.0" - global-modules "2.0.0" - import-local "2.0.0" - interpret "1.2.0" - loader-utils "1.2.3" - supports-color "6.1.0" - v8-compile-cache "2.0.3" - yargs "13.2.4" - -webpack-sources@^1.4.0, webpack-sources@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" -webpack@^4.42.1: - version "4.42.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.1.tgz#ae707baf091f5ca3ef9c38b884287cfe8f1983ef" - integrity sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.2.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" +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 "^4.1.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.6.0" - webpack-sources "^1.4.1" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.14, which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -xtend@^4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yargs-parser@^13.1.0: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" + 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" From 99d1114f7a0b397c31972dba78edadcbb2f6aee9 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 5 Feb 2024 14:25:00 +0100 Subject: [PATCH 46/68] fix: Make `startSpan`, `startSpanManual` and `startInactiveSpan` pick up the scopes at time of creation instead of termination (#10492) --- packages/core/src/baseclient.ts | 10 +- packages/core/src/tracing/trace.ts | 60 +++++++++--- packages/core/src/tracing/transaction.ts | 5 + packages/core/test/lib/tracing/trace.test.ts | 85 +++++++++++++++++ packages/node/test/performance.test.ts | 98 +++++++++++++++++++- 5 files changed, 242 insertions(+), 16 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index b3eb10f686d7..b7f00e14baef 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -222,8 +222,11 @@ export abstract class BaseClient implements Client { let eventId: string | undefined = hint && hint.event_id; + const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; + const capturedSpanScope: Scope | undefined = sdkProcessingMetadata.capturedSpanScope; + this._process( - this._captureEvent(event, hint, scope).then(result => { + this._captureEvent(event, hint, capturedSpanScope || scope).then(result => { eventId = result; }), ); @@ -753,7 +756,10 @@ export abstract class BaseClient implements Client { const dataCategory: DataCategory = eventType === 'replay_event' ? 'replay' : eventType; - return this._prepareEvent(event, hint, scope) + const sdkProcessingMetadata = event.sdkProcessingMetadata || {}; + const capturedSpanIsolationScope: Scope | undefined = sdkProcessingMetadata.capturedSpanIsolationScope; + + return this._prepareEvent(event, hint, scope, capturedSpanIsolationScope) .then(prepared => { if (prepared === null) { this.recordDroppedEvent('event_processor', dataCategory, event); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 1f8b45d5fd97..832180ef3c72 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,6 +1,6 @@ -import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; +import type { Scope, Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; -import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; +import { addNonEnumerableProperty, dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { getCurrentScope, withScope } from '../exports'; @@ -189,20 +189,22 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { return undefined; } + const isolationScope = getIsolationScope(); + const scope = getCurrentScope(); + + let span: Span | undefined; + if (parentSpan) { // eslint-disable-next-line deprecation/deprecation - return parentSpan.startChild(ctx); + span = parentSpan.startChild(ctx); } else { - const isolationScope = getIsolationScope(); - const scope = getCurrentScope(); - const { traceId, dsc, parentSpanId, sampled } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; // eslint-disable-next-line deprecation/deprecation - return hub.startTransaction({ + span = hub.startTransaction({ traceId, parentSpanId, parentSampled: sampled, @@ -214,6 +216,10 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { }, }); } + + setCapturedScopesOnSpan(span, scope, isolationScope); + + return span; } /** @@ -335,20 +341,21 @@ function createChildSpanOrTransaction( return undefined; } + const isolationScope = getIsolationScope(); + const scope = getCurrentScope(); + + let span: Span | undefined; if (parentSpan) { // eslint-disable-next-line deprecation/deprecation - return parentSpan.startChild(ctx); + span = parentSpan.startChild(ctx); } else { - const isolationScope = getIsolationScope(); - const scope = getCurrentScope(); - const { traceId, dsc, parentSpanId, sampled } = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext(), }; // eslint-disable-next-line deprecation/deprecation - return hub.startTransaction({ + span = hub.startTransaction({ traceId, parentSpanId, parentSampled: sampled, @@ -360,6 +367,10 @@ function createChildSpanOrTransaction( }, }); } + + setCapturedScopesOnSpan(span, scope, isolationScope); + + return span; } /** @@ -379,3 +390,28 @@ function normalizeContext(context: StartSpanOptions): TransactionContext { return context; } + +const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; +const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; + +type SpanWithScopes = Span & { + [SCOPE_ON_START_SPAN_FIELD]?: Scope; + [ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: Scope; +}; + +function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, isolationScope: Scope): void { + if (span) { + addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, isolationScope); + addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, scope); + } +} + +/** + * Grabs the scope and isolation scope off a span that were active when the span was started. + */ +export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationScope?: Scope } { + return { + scope: (span as SpanWithScopes)[SCOPE_ON_START_SPAN_FIELD], + isolationScope: (span as SpanWithScopes)[ISOLATION_SCOPE_ON_START_SPAN_FIELD], + }; +} diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 490714636fe9..026723929471 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -19,6 +19,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; +import { getCapturedScopesOnSpan } from './trace'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { @@ -303,6 +304,8 @@ export class Transaction extends SpanClass implements TransactionInterface { }); } + const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this); + // eslint-disable-next-line deprecation/deprecation const { metadata } = this; // eslint-disable-next-line deprecation/deprecation @@ -324,6 +327,8 @@ export class Transaction extends SpanClass implements TransactionInterface { type: 'transaction', sdkProcessingMetadata: { ...metadata, + capturedSpanScope, + capturedSpanIsolationScope, dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, ...(source && { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 265a34195f71..4c9190e56b6a 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,3 +1,4 @@ +import type { Span as SpanType } from '@sentry/types'; import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -387,9 +388,50 @@ describe('startSpan', () => { transactionContext: expect.objectContaining({ name: 'outer', parentSampled: undefined }), }); }); + + it('includes the scope at the time the span was started when finished', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction(event) { + resolve(event); + return event; + }, + }), + ), + ); + }); + + withScope(scope1 => { + scope1.setTag('scope', 1); + startSpanManual({ name: 'my-span' }, span => { + withScope(scope2 => { + scope2.setTag('scope', 2); + span?.end(); + }); + }); + }); + + expect(await transactionEventPromise).toMatchObject({ + tags: { + scope: 1, + }, + }); + }); }); describe('startSpanManual', () => { + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); + client = new TestClient(options); + hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + it('creates & finishes span', async () => { startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { expect(span).toBeDefined(); @@ -492,6 +534,14 @@ describe('startSpanManual', () => { }); describe('startInactiveSpan', () => { + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); + client = new TestClient(options); + hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + }); + it('creates & finishes span', async () => { const span = startInactiveSpan({ name: 'GET users/[id]' }); @@ -571,6 +621,41 @@ describe('startInactiveSpan', () => { expect(span).toBeDefined(); }); }); + + it('includes the scope at the time the span was started when finished', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + beforeSendTransaction(event) { + resolve(event); + return event; + }, + }), + ), + ); + }); + + let span: SpanType | undefined; + + withScope(scope => { + scope.setTag('scope', 1); + span = startInactiveSpan({ name: 'my-span' }); + }); + + withScope(scope => { + scope.setTag('scope', 2); + span?.end(); + }); + + expect(await transactionEventPromise).toMatchObject({ + tags: { + scope: 1, + }, + }); + }); }); describe('continueTrace', () => { diff --git a/packages/node/test/performance.test.ts b/packages/node/test/performance.test.ts index 0f57dd4166e6..513a3e95a7c0 100644 --- a/packages/node/test/performance.test.ts +++ b/packages/node/test/performance.test.ts @@ -1,5 +1,13 @@ -import { setAsyncContextStrategy, setCurrentClient, startSpan, startSpanManual } from '@sentry/core'; -import type { TransactionEvent } from '@sentry/types'; +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'; @@ -147,4 +155,90 @@ describe('startSpanManual()', () => { expect(transactionEvent.spans).toContainEqual(expect.objectContaining({ description: '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, + }, + }); + }); }); From 810cb70333d5a81e04aa349b1a748beca9821d40 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 5 Feb 2024 15:26:59 +0100 Subject: [PATCH 47/68] feat(vue): Implement vue `browserTracingIntegration()` (#10477) This replaces the `vueRouterInstrumentation` and allows to deprecate browser tracing in the vue package. Waiting for https://github.com/getsentry/sentry-javascript/pull/10476, then we should put these changes on top of the E2E test to verify it still works. --- .../test-applications/vue-3/src/main.ts | 5 +- .../vue-3/tests/performance.test.ts | 13 +- packages/vue/README.md | 12 +- packages/vue/src/browserTracingIntegration.ts | 74 ++++ packages/vue/src/index.ts | 2 + packages/vue/src/router.ts | 164 ++++--- packages/vue/test/router.test.ts | 405 ++++++++++++++++-- 7 files changed, 549 insertions(+), 126 deletions(-) create mode 100644 packages/vue/src/browserTracingIntegration.ts diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts index 503a9e44d14f..997c74fa0740 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts @@ -5,6 +5,7 @@ import App from './App.vue'; import router from './router'; import * as Sentry from '@sentry/vue'; +import { browserTracingIntegration } from '@sentry/vue'; const app = createApp(App); @@ -13,8 +14,8 @@ Sentry.init({ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, tracesSampleRate: 1.0, integrations: [ - new Sentry.BrowserTracing({ - routingInstrumentation: Sentry.vueRouterInstrumentation(router), + browserTracingIntegration({ + router, }), ], tunnel: `http://localhost:3031/`, // proxy server 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 732ec98a54f4..2210c92e5dfd 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 @@ -14,12 +14,10 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = contexts: { trace: { data: { - params: { - id: '456', - }, 'sentry.source': 'route', 'sentry.origin': 'auto.pageload.vue', 'sentry.op': 'pageload', + 'params.id': '456', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -52,12 +50,10 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) contexts: { trace: { data: { - params: { - id: '123', - }, 'sentry.source': 'route', 'sentry.origin': 'auto.navigation.vue', 'sentry.op': 'navigation', + 'params.id': '456', }, op: 'navigation', origin: 'auto.navigation.vue', @@ -65,10 +61,7 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) }, transaction: '/users/:id', transaction_info: { - // So this is weird. The source is set to custom although the route doesn't have a name. - // This also only happens during a navigation. A pageload will set the source as 'route'. - // TODO: Figure out what's going on here. - source: 'custom', + source: 'route', }, }); }); diff --git a/packages/vue/README.md b/packages/vue/README.md index c8fd91af2b24..378b15eafd0d 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -28,6 +28,10 @@ const app = createApp({ Sentry.init({ app, dsn: '__PUBLIC_DSN__', + integrations: [ + // Or omit `router` if you're not using vue-router + Sentry.browserTracingIntegration({ router }), + ], }); ``` @@ -42,12 +46,16 @@ import * as Sentry from '@sentry/vue' Sentry.init({ Vue: Vue, dsn: '__PUBLIC_DSN__', -}) + integrations: [ + // Or omit `router` if you're not using vue-router + Sentry.browserTracingIntegration({ router }), + ], +}); new Vue({ el: '#app', router, components: { App }, template: '' -}) +}); ``` diff --git a/packages/vue/src/browserTracingIntegration.ts b/packages/vue/src/browserTracingIntegration.ts new file mode 100644 index 000000000000..d78bdd992d6b --- /dev/null +++ b/packages/vue/src/browserTracingIntegration.ts @@ -0,0 +1,74 @@ +import { + browserTracingIntegration as originalBrowserTracingIntegration, + startBrowserTracingNavigationSpan, +} from '@sentry/browser'; +import type { Integration, StartSpanOptions } from '@sentry/types'; +import { instrumentVueRouter } from './router'; + +// 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. +export type Route = { + /** Unparameterized URL */ + path: string; + /** + * Query params (keys map to null when there is no value associated, e.g. "?foo" and to an array when there are + * multiple query params that have the same key, e.g. "?foo&foo=bar") + */ + query: Record; + /** Route name (VueRouter provides a way to give routes individual names) */ + name?: string | symbol | null | undefined; + /** Evaluated parameters */ + params: Record; + /** All the matched route objects as defined in VueRouter constructor */ + matched: { path: string }[]; +}; + +interface VueRouter { + onError: (fn: (err: Error) => void) => void; + beforeEach: (fn: (to: Route, from: Route, next?: () => void) => void) => void; +} + +type VueBrowserTracingIntegrationOptions = Parameters[0] & { + /** + * If a router is specified, navigation spans will be created based on the router. + */ + router?: VueRouter; + + /** + * What to use for route labels. + * By default, we use route.name (if set) and else the path. + * + * Default: 'name' + */ + routeLabel?: 'name' | 'path'; +}; + +/** + * A custom BrowserTracing integration for Vue. + */ +export function browserTracingIntegration(options: VueBrowserTracingIntegrationOptions = {}): Integration { + // If router is not passed, we just use the normal implementation + if (!options.router) { + return originalBrowserTracingIntegration(options); + } + + const integration = originalBrowserTracingIntegration({ + ...options, + instrumentNavigation: false, + }); + + const { router, instrumentNavigation = true, instrumentPageLoad = true, routeLabel = 'name' } = options; + + return { + ...integration, + afterAllSetup(client) { + integration.afterAllSetup(client); + + const startNavigationSpan = (options: StartSpanOptions): void => { + startBrowserTracingNavigationSpan(client, options); + }; + + instrumentVueRouter(router, { routeLabel, instrumentNavigation, instrumentPageLoad }, startNavigationSpan); + }, + }; +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 030324af9430..0b9626ee185d 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -1,7 +1,9 @@ export * from '@sentry/browser'; export { init } from './sdk'; +// eslint-disable-next-line deprecation/deprecation export { vueRouterInstrumentation } from './router'; +export { browserTracingIntegration } from './browserTracingIntegration'; export { attachErrorHandler } from './errorhandler'; export { createTracingMixins } from './tracing'; export { diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 98c18ae80691..b7f3fd0466b0 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -1,6 +1,6 @@ import { WINDOW, captureException } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; -import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; +import type { SpanAttributes, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getActiveTransaction } from './tracing'; @@ -50,6 +50,8 @@ interface VueRouter { * * `routeLabel`: Set this to `route` to opt-out of using `route.name` for transaction names. * * @param router The Vue Router instance that is used + * + * @deprecated Use `browserTracingIntegration()` from `@sentry/vue` instead - this includes the vue router instrumentation. */ export function vueRouterInstrumentation( router: VueRouter, @@ -60,10 +62,6 @@ export function vueRouterInstrumentation( startTransactionOnPageLoad: boolean = true, startTransactionOnLocationChange: boolean = true, ) => { - const tags = { - 'routing.instrumentation': 'vue-router', - }; - // We have to start the pageload transaction as early as possible (before the router's `beforeEach` hook // is called) to not miss child spans of the pageload. // We check that window & window.location exists in order to not run this code in SSR environments. @@ -71,77 +69,107 @@ export function vueRouterInstrumentation( startTransaction({ name: WINDOW.location.pathname, op: 'pageload', - origin: 'auto.pageload.vue', - tags, - data: { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); } - router.onError(error => captureException(error, { mechanism: { handled: false } })); - - router.beforeEach((to, from, next) => { - // According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2 - // https://router.vuejs.org/api/#router-start-location - // https://next.router.vuejs.org/api/#start-location - - // from.name: - // - Vue 2: null - // - Vue 3: undefined - // hence only '==' instead of '===', because `undefined == null` evaluates to `true` - const isPageLoadNavigation = from.name == null && from.matched.length === 0; - - const data: Record = { - params: to.params, - query: to.query, - }; - - // Determine a name for the routing transaction and where that name came from - let transactionName: string = to.path; - let transactionSource: TransactionSource = 'url'; - if (to.name && options.routeLabel !== 'path') { - transactionName = to.name.toString(); - transactionSource = 'custom'; - } else if (to.matched[0] && to.matched[0].path) { - transactionName = to.matched[0].path; - transactionSource = 'route'; - } + instrumentVueRouter( + router, + { + routeLabel: options.routeLabel || 'name', + instrumentNavigation: startTransactionOnLocationChange, + instrumentPageLoad: startTransactionOnPageLoad, + }, + startTransaction, + ); + }; +} - if (startTransactionOnPageLoad && isPageLoadNavigation) { - // eslint-disable-next-line deprecation/deprecation - const pageloadTransaction = getActiveTransaction(); - if (pageloadTransaction) { - const attributes = spanToJSON(pageloadTransaction).data || {}; - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { - pageloadTransaction.updateName(transactionName); - pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); - } - // TODO: We need to flatten these to make them attributes - // eslint-disable-next-line deprecation/deprecation - pageloadTransaction.setData('params', data.params); - // eslint-disable-next-line deprecation/deprecation - pageloadTransaction.setData('query', data.query); - } +/** + * Instrument the Vue router to create navigation spans. + */ +export function instrumentVueRouter( + router: VueRouter, + options: { + routeLabel: 'name' | 'path'; + instrumentPageLoad: boolean; + instrumentNavigation: boolean; + }, + startNavigationSpanFn: (context: TransactionContext) => void, +): void { + router.onError(error => captureException(error, { mechanism: { handled: false } })); + + router.beforeEach((to, from, next) => { + // According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2 + // https://router.vuejs.org/api/#router-start-location + // https://next.router.vuejs.org/api/#start-location + + // from.name: + // - Vue 2: null + // - Vue 3: undefined + // hence only '==' instead of '===', because `undefined == null` evaluates to `true` + const isPageLoadNavigation = from.name == null && from.matched.length === 0; + + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + }; + + for (const key of Object.keys(to.params)) { + attributes[`params.${key}`] = to.params[key]; + } + for (const key of Object.keys(to.query)) { + const value = to.query[key]; + if (value) { + attributes[`query.${key}`] = value; } + } - if (startTransactionOnLocationChange && !isPageLoadNavigation) { - data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource; - startTransaction({ - name: transactionName, - op: 'navigation', - origin: 'auto.navigation.vue', - tags, - data, + // Determine a name for the routing transaction and where that name came from + let transactionName: string = to.path; + let transactionSource: TransactionSource = 'url'; + if (to.name && options.routeLabel !== 'path') { + transactionName = to.name.toString(); + transactionSource = 'custom'; + } else if (to.matched[0] && to.matched[0].path) { + transactionName = to.matched[0].path; + transactionSource = 'route'; + } + + if (options.instrumentPageLoad && isPageLoadNavigation) { + // eslint-disable-next-line deprecation/deprecation + const pageloadTransaction = getActiveTransaction(); + if (pageloadTransaction) { + const existingAttributes = spanToJSON(pageloadTransaction).data || {}; + if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { + pageloadTransaction.updateName(transactionName); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); + } + // Set router attributes on the existing pageload transaction + // This will the origin, and add params & query attributes + pageloadTransaction.setAttributes({ + ...attributes, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', }); } + } - // Vue Router 4 no longer exposes the `next` function, so we need to - // check if it's available before calling it. - // `next` needs to be called in Vue Router 3 so that the hook is resolved. - if (next) { - next(); - } - }); - }; + if (options.instrumentNavigation && !isPageLoadNavigation) { + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource; + startNavigationSpanFn({ + name: transactionName, + op: 'navigation', + attributes, + }); + } + + // Vue Router 4 no longer exposes the `next` function, so we need to + // check if it's available before calling it. + // `next` needs to be called in Vue Router 3 so that the hook is resolved. + if (next) { + next(); + } + }); } diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 061bcdd3e1f9..7d45889be864 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,9 +1,9 @@ import * as SentryBrowser from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import type { Transaction } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import type { SpanAttributes, Transaction } from '@sentry/types'; -import { vueRouterInstrumentation } from '../src'; import type { Route } from '../src/router'; +import { instrumentVueRouter, vueRouterInstrumentation } from '../src/router'; import * as vueTracing from '../src/tracing'; const captureExceptionSpy = jest.spyOn(SentryBrowser, 'captureException'); @@ -13,7 +13,6 @@ const mockVueRouter = { beforeEach: jest.fn void) => void]>(), }; -const mockStartTransaction = jest.fn(); const mockNext = jest.fn(); const testRoutes: Record = { @@ -52,7 +51,10 @@ const testRoutes: Record = { }, }; +/* eslint-disable deprecation/deprecation */ describe('vueRouterInstrumentation()', () => { + const mockStartTransaction = jest.fn(); + afterEach(() => { jest.clearAllMocks(); }); @@ -101,16 +103,12 @@ describe('vueRouterInstrumentation()', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenCalledWith({ name: transactionName, - data: { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource, - params: to.params, - query: to.query, + ...getAttributesForRoute(to), }, op: 'navigation', - origin: 'auto.navigation.vue', - tags: { - 'routing.instrumentation': 'vue-router', - }, }); expect(mockNext).toHaveBeenCalledTimes(1); @@ -128,6 +126,7 @@ describe('vueRouterInstrumentation()', () => { updateName: jest.fn(), setData: jest.fn(), setAttribute: jest.fn(), + setAttributes: jest.fn(), metadata: {}, }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { @@ -145,14 +144,11 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - data: { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', - origin: 'auto.pageload.vue', - tags: { - 'routing.instrumentation': 'vue-router', - }, }); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; @@ -165,8 +161,10 @@ describe('vueRouterInstrumentation()', () => { expect(mockedTxn.updateName).toHaveBeenCalledWith(transactionName); expect(mockedTxn.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); - expect(mockedTxn.setData).toHaveBeenNthCalledWith(1, 'params', to.params); - expect(mockedTxn.setData).toHaveBeenNthCalledWith(2, 'query', to.query); + expect(mockedTxn.setAttributes).toHaveBeenCalledWith({ + ...getAttributesForRoute(to), + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + }); expect(mockNext).toHaveBeenCalledTimes(1); }, @@ -189,16 +187,12 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - data: { + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - params: to.params, - query: to.query, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + ...getAttributesForRoute(to), }, op: 'navigation', - origin: 'auto.navigation.vue', - tags: { - 'routing.instrumentation': 'vue-router', - }, }); }); @@ -219,16 +213,12 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: 'login-screen', - data: { + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - params: to.params, - query: to.query, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + ...getAttributesForRoute(to), }, op: 'navigation', - origin: 'auto.navigation.vue', - tags: { - 'routing.instrumentation': 'vue-router', - }, }); }); @@ -237,6 +227,7 @@ describe('vueRouterInstrumentation()', () => { updateName: jest.fn(), setData: jest.fn(), setAttribute: jest.fn(), + setAttributes: jest.fn(), name: '', toJSON: () => ({ data: { @@ -259,14 +250,11 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - data: { + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', }, op: 'pageload', - origin: 'auto.pageload.vue', - tags: { - 'routing.instrumentation': 'vue-router', - }, }); // now we give the transaction a custom name, thereby simulating what would @@ -278,13 +266,20 @@ describe('vueRouterInstrumentation()', () => { }, }); + const to = testRoutes['normalRoute1']; + const from = testRoutes['initialPageloadRoute']; + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; - beforeEachCallback(testRoutes['normalRoute1'], testRoutes['initialPageloadRoute'], mockNext); + beforeEachCallback(to, from, mockNext); expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).not.toHaveBeenCalled(); expect(mockedTxn.setAttribute).not.toHaveBeenCalled(); + expect(mockedTxn.setAttributes).toHaveBeenCalledWith({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + ...getAttributesForRoute(to), + }); expect(mockedTxn.name).toEqual('customTxnName'); }); @@ -346,16 +341,338 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - data: { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + ...getAttributesForRoute(to), + }, + op: 'navigation', + }); + }); +}); +/* eslint-enable deprecation/deprecation */ + +describe('instrumentVueRouter()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return instrumentation that instruments VueRouter.onError', () => { + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + // check + expect(mockVueRouter.onError).toHaveBeenCalledTimes(1); + + const onErrorCallback = mockVueRouter.onError.mock.calls[0][0]; + + const testError = new Error(); + onErrorCallback(testError); + + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenCalledWith(testError, { mechanism: { handled: false } }); + }); + + it.each([ + ['normalRoute1', 'normalRoute2', '/accounts/:accountId', 'route'], + ['normalRoute2', 'namedRoute', 'login-screen', 'custom'], + ['normalRoute2', 'unmatchedRoute', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'], + ])( + 'should return instrumentation that instruments VueRouter.beforeEach(%s, %s) for navigations', + (fromKey, toKey, transactionName, transactionSource) => { + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + // check + expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const from = testRoutes[fromKey]; + const to = testRoutes[toKey]; + beforeEachCallback(to, from, mockNext); + + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith({ + name: transactionName, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource, + ...getAttributesForRoute(to), + }, + op: 'navigation', + }); + + expect(mockNext).toHaveBeenCalledTimes(1); + }, + ); + + it.each([ + ['initialPageloadRoute', 'normalRoute1', '/books/:bookId/chapter/:chapterId', 'route'], + ['initialPageloadRoute', 'namedRoute', 'login-screen', 'custom'], + ['initialPageloadRoute', 'unmatchedRoute', '/e8733846-20ac-488c-9871-a5cbcb647294', 'url'], + ])( + 'should return instrumentation that instruments VueRouter.beforeEach(%s, %s) for pageloads', + (fromKey, toKey, transactionName, transactionSource) => { + const mockedTxn = { + updateName: jest.fn(), + setData: jest.fn(), + setAttribute: jest.fn(), + setAttributes: jest.fn(), + metadata: {}, + }; + + jest.spyOn(vueTracing, 'getActiveTransaction').mockImplementation(() => mockedTxn as unknown as Transaction); + + const mockStartSpan = jest.fn().mockImplementation(_ => { + return mockedTxn; + }); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + // no span is started for page load + expect(mockStartSpan).not.toHaveBeenCalled(); + + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const from = testRoutes[fromKey]; + const to = testRoutes[toKey]; + + beforeEachCallback(to, from, mockNext); + expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); + + expect(mockedTxn.updateName).toHaveBeenCalledWith(transactionName); + expect(mockedTxn.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); + expect(mockedTxn.setAttributes).toHaveBeenCalledWith({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + ...getAttributesForRoute(to), + }); + + expect(mockNext).toHaveBeenCalledTimes(1); + }, + ); + + it('allows to configure routeLabel=path', () => { + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'path', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + // check + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const from = testRoutes.normalRoute1; + const to = testRoutes.namedRoute; + beforeEachCallback(to, from, mockNext); + + // first startTx call happens when the instrumentation is initialized (for pageloads) + expect(mockStartSpan).toHaveBeenLastCalledWith({ + name: '/login', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - params: to.params, - query: to.query, + ...getAttributesForRoute(to), }, op: 'navigation', - origin: 'auto.navigation.vue', - tags: { - 'routing.instrumentation': 'vue-router', + }); + }); + + it('allows to configure routeLabel=name', () => { + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + // check + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const from = testRoutes.normalRoute1; + const to = testRoutes.namedRoute; + beforeEachCallback(to, from, mockNext); + + // first startTx call happens when the instrumentation is initialized (for pageloads) + expect(mockStartSpan).toHaveBeenLastCalledWith({ + name: 'login-screen', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + ...getAttributesForRoute(to), }, + op: 'navigation', + }); + }); + + it("doesn't overwrite a pageload transaction name it was set to custom before the router resolved the route", () => { + const mockedTxn = { + updateName: jest.fn(), + setData: jest.fn(), + setAttribute: jest.fn(), + setAttributes: jest.fn(), + name: '', + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }), + }; + const mockStartSpan = jest.fn().mockImplementation(_ => { + return mockedTxn; + }); + jest.spyOn(vueTracing, 'getActiveTransaction').mockImplementation(() => mockedTxn as unknown as Transaction); + + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + // check for transaction start + expect(mockStartSpan).not.toHaveBeenCalled(); + + // now we give the transaction a custom name, thereby simulating what would + // happen when users use the `beforeNavigate` hook + mockedTxn.name = 'customTxnName'; + mockedTxn.toJSON = () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }); + + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const to = testRoutes['normalRoute1']; + const from = testRoutes['initialPageloadRoute']; + + beforeEachCallback(to, from, mockNext); + + expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); + + expect(mockedTxn.updateName).not.toHaveBeenCalled(); + expect(mockedTxn.setAttribute).not.toHaveBeenCalled(); + expect(mockedTxn.setAttributes).toHaveBeenCalledWith({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + ...getAttributesForRoute(to), + }); + expect(mockedTxn.name).toEqual('customTxnName'); + }); + + test.each([ + [false, 0], + [true, 1], + ])( + 'should return instrumentation that considers the instrumentPageLoad = %p', + (instrumentPageLoad, expectedCallsAmount) => { + const mockedTxn = { + updateName: jest.fn(), + setData: jest.fn(), + setAttribute: jest.fn(), + setAttributes: jest.fn(), + name: '', + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }), + }; + jest.spyOn(vueTracing, 'getActiveTransaction').mockImplementation(() => mockedTxn as unknown as Transaction); + + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad, instrumentNavigation: true }, + mockStartSpan, + ); + + // check + expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); + + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + beforeEachCallback(testRoutes['normalRoute1'], testRoutes['initialPageloadRoute'], mockNext); + + expect(mockedTxn.updateName).toHaveBeenCalledTimes(expectedCallsAmount); + expect(mockStartSpan).not.toHaveBeenCalled(); + }, + ); + + test.each([ + [false, 0], + [true, 1], + ])( + 'should return instrumentation that considers the instrumentNavigation = %p', + (instrumentNavigation, expectedCallsAmount) => { + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation }, + mockStartSpan, + ); + + // check + expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); + + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + beforeEachCallback(testRoutes['normalRoute2'], testRoutes['normalRoute1'], mockNext); + + expect(mockStartSpan).toHaveBeenCalledTimes(expectedCallsAmount); + }, + ); + + it("doesn't throw when `next` is not available in the beforeEach callback (Vue Router 4)", () => { + const mockStartSpan = jest.fn(); + instrumentVueRouter( + mockVueRouter, + { routeLabel: 'path', instrumentPageLoad: true, instrumentNavigation: true }, + mockStartSpan, + ); + + const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; + + const from = testRoutes.normalRoute1; + const to = testRoutes.namedRoute; + beforeEachCallback(to, from, undefined); + + // first startTx call happens when the instrumentation is initialized (for pageloads) + expect(mockStartSpan).toHaveBeenLastCalledWith({ + name: '/login', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + ...getAttributesForRoute(to), + }, + op: 'navigation', }); }); }); + +// Small helper function to get flattened attributes for test comparison +function getAttributesForRoute(route: Route): SpanAttributes { + const { params, query } = route; + + const attributes: SpanAttributes = {}; + + for (const key of Object.keys(params)) { + attributes[`params.${key}`] = params[key]; + } + for (const key of Object.keys(query)) { + const value = query[key]; + if (value) { + attributes[`query.${key}`] = value; + } + } + + return attributes; +} From baf5f90dddb20585f8e0f1ab0271ce21b73c08d6 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 5 Feb 2024 14:39:40 +0000 Subject: [PATCH 48/68] feat(remix): Add custom `browserTracingIntegration` (#10442) Adds a custom `browserTracingIntegration` for `@sentry/remix`. This also marks `remixRouterInstrumentation` as `deprecated`. It's still functional, and the logic checks the existence of globally defined `_customStartTransaction` to decide how to behave. Added another integration test application _for now_ using Remix v2 and the new `browserTracingIntegration` until we switch to it completely. --- .github/workflows/build.yml | 5 +- .../create-remix-app-v2/remix.config.js | 1 + packages/remix/README.md | 39 ++-- packages/remix/package.json | 5 +- .../src/client/browserTracingIntegration.ts | 41 ++++ packages/remix/src/client/performance.tsx | 192 +++++++++++++----- packages/remix/src/index.client.tsx | 20 +- packages/remix/src/index.server.ts | 2 + .../entry.client.tsx | 12 ++ .../entry.server.tsx | 30 +++ .../app_v2_tracingIntegration/root.tsx | 73 +++++++ .../routes/action-json-response.$id.tsx | 2 + .../routes/capture-exception.tsx | 2 + .../routes/capture-message.tsx | 2 + .../routes/error-boundary-capture.$id.tsx | 2 + .../routes/index.tsx | 2 + .../routes/loader-defer-response.$id.tsx | 2 + .../routes/loader-json-response.$id.tsx | 2 + .../routes/loader-throw-response.$id.tsx | 2 + .../routes/manual-tracing.$id.tsx | 2 + .../routes/scope-bleed.$id.tsx | 2 + .../server-side-unexpected-errors.$id.tsx | 2 + .../routes/ssr-error.tsx | 2 + .../routes/throw-redirect.tsx | 2 + .../remix/test/integration/remix.config.js | 3 +- 25 files changed, 367 insertions(+), 82 deletions(-) create mode 100644 packages/remix/src/client/browserTracingIntegration.ts create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/entry.client.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/entry.server.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/root.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/action-json-response.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-exception.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-message.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/error-boundary-capture.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/index.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-defer-response.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-json-response.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-throw-response.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/manual-tracing.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/scope-bleed.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/server-side-unexpected-errors.$id.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/ssr-error.tsx create mode 100644 packages/remix/test/integration/app_v2_tracingIntegration/routes/throw-redirect.tsx diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b5929258cb8..f151f9ec33b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -905,7 +905,7 @@ jobs: yarn test job_remix_integration_tests: - name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) Tests + name: Remix v${{ matrix.remix }} (Node ${{ matrix.node }}) ${{ matrix.tracingIntegration && 'TracingIntegration'}} Tests needs: [job_get_metadata, job_build] if: needs.job_get_metadata.outputs.changed_remix == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 @@ -921,6 +921,8 @@ jobs: remix: 1 - node: 16 remix: 1 + - tracingIntegration: true + remix: 2 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -938,6 +940,7 @@ jobs: env: NODE_VERSION: ${{ matrix.node }} REMIX_VERSION: ${{ matrix.remix }} + TRACING_INTEGRATION: ${{ matrix.tracingIntegration }} run: | cd packages/remix yarn test:integration:ci diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/remix.config.js b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/remix.config.js index cb3c8c7a9fb7..92ed1ddc32eb 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/remix.config.js +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/remix.config.js @@ -6,4 +6,5 @@ module.exports = { // serverBuildPath: 'build/index.js', // publicPath: '/build/', serverModuleFormat: 'cjs', + entry, }; diff --git a/packages/remix/README.md b/packages/remix/README.md index c51820980e91..ae2dfbeff53a 100644 --- a/packages/remix/README.md +++ b/packages/remix/README.md @@ -12,27 +12,26 @@ ## General -This package is a wrapper around `@sentry/node` for the server and `@sentry/react` for the client, with added functionality related to Remix. +This package is a wrapper around `@sentry/node` for the server and `@sentry/react` for the client, with added +functionality related to Remix. To use this SDK, initialize Sentry in your Remix entry points for both the client and server. ```ts // entry.client.tsx -import { useLocation, useMatches } from "@remix-run/react"; -import * as Sentry from "@sentry/remix"; -import { useEffect } from "react"; +import { useLocation, useMatches } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { useEffect } from 'react'; Sentry.init({ - dsn: "__DSN__", + dsn: '__DSN__', tracesSampleRate: 1, integrations: [ - new Sentry.BrowserTracing({ - routingInstrumentation: Sentry.remixRouterInstrumentation( - useEffect, - useLocation, - useMatches, - ), + Sentry.browserTracingIntegration({ + useEffect, + useLocation, + useMatches, }), ], // ... @@ -42,19 +41,20 @@ Sentry.init({ ```ts // entry.server.tsx -import { prisma } from "~/db.server"; +import { prisma } from '~/db.server'; -import * as Sentry from "@sentry/remix"; +import * as Sentry from '@sentry/remix'; Sentry.init({ - dsn: "__DSN__", + dsn: '__DSN__', tracesSampleRate: 1, integrations: [new Sentry.Integrations.Prisma({ client: prisma })], // ... }); ``` -Also, wrap your Remix root with `withSentry` to catch React component errors and to get parameterized router transactions. +Also, wrap your Remix root with `withSentry` to catch React component errors and to get parameterized router +transactions. ```ts // root.tsx @@ -139,8 +139,11 @@ Sentry.captureEvent({ ## Sourcemaps and Releases -The Remix SDK provides a script that automatically creates a release and uploads sourcemaps. To generate sourcemaps with Remix, you need to call `remix build` with the `--sourcemap` option. +The Remix SDK provides a script that automatically creates a release and uploads sourcemaps. To generate sourcemaps with +Remix, you need to call `remix build` with the `--sourcemap` option. -On release, call `sentry-upload-sourcemaps` to upload source maps and create a release. To see more details on how to use the command, call `sentry-upload-sourcemaps --help`. +On release, call `sentry-upload-sourcemaps` to upload source maps and create a release. To see more details on how to +use the command, call `sentry-upload-sourcemaps --help`. -For more advanced configuration, [directly use `sentry-cli` to upload source maps.](https://github.com/getsentry/sentry-cli). +For more advanced configuration, +[directly use `sentry-cli` to upload source maps.](https://github.com/getsentry/sentry-cli). diff --git a/packages/remix/package.json b/packages/remix/package.json index b9e22128ece4..2133813e857a 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -70,11 +70,12 @@ "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "test": "yarn test:unit", - "test:integration": "run-s test:integration:v1 test:integration:v2", + "test:integration": "run-s test:integration:v1 test:integration:v2 test:integration:tracingIntegration", "test:integration:v1": "run-s test:integration:clean test:integration:prepare test:integration:client test:integration:server", "test:integration:v2": "export REMIX_VERSION=2 && run-s test:integration:v1", + "test:integration:tracingIntegration": "export TRACING_INTEGRATION=true && run-s test:integration:v2", "test:integration:ci": "run-s test:integration:clean test:integration:prepare test:integration:client:ci test:integration:server", - "test:integration:prepare": "(cd test/integration && yarn)", + "test:integration:prepare": "(cd test/integration && yarn install)", "test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)", "test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/ --project='chromium'", "test:integration:client:ci": "yarn test:integration:client --reporter='line'", diff --git a/packages/remix/src/client/browserTracingIntegration.ts b/packages/remix/src/client/browserTracingIntegration.ts new file mode 100644 index 000000000000..c0eb1a97148d --- /dev/null +++ b/packages/remix/src/client/browserTracingIntegration.ts @@ -0,0 +1,41 @@ +import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; +import type { Integration } from '@sentry/types'; +import { setGlobals, startPageloadSpan } from './performance'; +import type { RemixBrowserTracingIntegrationOptions } from './performance'; +/** + * Creates a browser tracing integration for Remix applications. + * This integration will create pageload and navigation spans. + */ +export function browserTracingIntegration(options: RemixBrowserTracingIntegrationOptions): Integration { + if (options.instrumentPageLoad === undefined) { + options.instrumentPageLoad = true; + } + + if (options.instrumentNavigation === undefined) { + options.instrumentNavigation = true; + } + + setGlobals({ + useEffect: options.useEffect, + useLocation: options.useLocation, + useMatches: options.useMatches, + instrumentNavigation: options.instrumentNavigation, + }); + + const browserTracingIntegrationInstance = originalBrowserTracingIntegration({ + ...options, + instrumentPageLoad: false, + instrumentNavigation: false, + }); + + return { + ...browserTracingIntegrationInstance, + afterAllSetup(client) { + browserTracingIntegrationInstance.afterAllSetup(client); + + if (options.instrumentPageLoad) { + startPageloadSpan(); + } + }, + }; +} diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index fc395e8ddedc..10e91837149d 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -1,46 +1,58 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import type { ErrorBoundaryProps } from '@sentry/react'; -import { WINDOW, withErrorBoundary } from '@sentry/react'; -import type { Transaction, TransactionContext } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getRootSpan } from '@sentry/core'; +import type { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; +import type { BrowserClient, ErrorBoundaryProps } from '@sentry/react'; +import { + WINDOW, + getClient, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + withErrorBoundary, +} from '@sentry/react'; +import type { Span, StartSpanOptions, Transaction, TransactionContext } from '@sentry/types'; import { isNodeEnv, logger } from '@sentry/utils'; import * as React from 'react'; import { DEBUG_BUILD } from '../utils/debug-build'; import { getFutureFlagsBrowser, readRemixVersionFromLoader } from '../utils/futureFlags'; -const DEFAULT_TAGS = { - 'routing.instrumentation': 'remix-router', -} as const; - -type Params = { +export type Params = { readonly [key in Key]: string | undefined; }; -interface RouteMatch { +export interface RouteMatch { params: Params; pathname: string; id: string; handle: unknown; } +export type UseEffect = (cb: () => void, deps: unknown[]) => void; -type UseEffect = (cb: () => void, deps: unknown[]) => void; -type UseLocation = () => { +export type UseLocation = () => { pathname: string; search?: string; hash?: string; state?: unknown; key?: unknown; }; -type UseMatches = () => RouteMatch[] | null; -let activeTransaction: Transaction | undefined; +export type UseMatches = () => RouteMatch[] | null; + +export type RemixBrowserTracingIntegrationOptions = Partial[0]> & { + useEffect?: UseEffect; + useLocation?: UseLocation; + useMatches?: UseMatches; +}; + +const DEFAULT_TAGS = { + 'routing.instrumentation': 'remix-router', +} as const; -let _useEffect: UseEffect; -let _useLocation: UseLocation; -let _useMatches: UseMatches; +let _useEffect: UseEffect | undefined; +let _useLocation: UseLocation | undefined; +let _useMatches: UseMatches | undefined; -let _customStartTransaction: (context: TransactionContext) => Transaction | undefined; -let _startTransactionOnLocationChange: boolean; +let _customStartTransaction: ((context: TransactionContext) => Span | undefined) | undefined; +let _instrumentNavigation: boolean | undefined; function getInitPathName(): string | undefined { if (WINDOW && WINDOW.location) { @@ -54,7 +66,65 @@ function isRemixV2(remixVersion: number | undefined): boolean { return remixVersion === 2 || getFutureFlagsBrowser()?.v2_errorBoundary || false; } +export function startPageloadSpan(): void { + const initPathName = getInitPathName(); + + if (!initPathName) { + return; + } + + const spanContext: StartSpanOptions = { + name: initPathName, + op: 'pageload', + origin: 'auto.pageload.remix', + tags: DEFAULT_TAGS, + metadata: { + source: 'url', + }, + }; + + // If _customStartTransaction is not defined, we know that we are using the browserTracingIntegration + if (!_customStartTransaction) { + const client = getClient(); + + if (!client) { + return; + } + + startBrowserTracingPageLoadSpan(client, spanContext); + } else { + _customStartTransaction(spanContext); + } +} + +function startNavigationSpan(matches: RouteMatch[]): void { + const spanContext: StartSpanOptions = { + name: matches[matches.length - 1].id, + op: 'navigation', + origin: 'auto.navigation.remix', + tags: DEFAULT_TAGS, + metadata: { + source: 'route', + }, + }; + + // If _customStartTransaction is not defined, we know that we are using the browserTracingIntegration + if (!_customStartTransaction) { + const client = getClient(); + + if (!client) { + return; + } + + startBrowserTracingNavigationSpan(client, spanContext); + } else { + _customStartTransaction(spanContext); + } +} + /** + * @deprecated Use `browserTracingIntegration` instead. + * * Creates a react-router v6 instrumention for Remix applications. * * This implementation is slightly different (and simpler) from the react-router instrumentation @@ -66,25 +136,17 @@ export function remixRouterInstrumentation(useEffect: UseEffect, useLocation: Us startTransactionOnPageLoad = true, startTransactionOnLocationChange = true, ): void => { - const initPathName = getInitPathName(); - if (startTransactionOnPageLoad && initPathName) { - activeTransaction = customStartTransaction({ - name: initPathName, - op: 'pageload', - origin: 'auto.pageload.remix', - tags: DEFAULT_TAGS, - metadata: { - source: 'url', - }, - }); - } - - _useEffect = useEffect; - _useLocation = useLocation; - _useMatches = useMatches; + setGlobals({ + useEffect, + useLocation, + useMatches, + instrumentNavigation: startTransactionOnLocationChange, + customStartTransaction, + }); - _customStartTransaction = customStartTransaction; - _startTransactionOnLocationChange = startTransactionOnLocationChange; + if (startTransactionOnPageLoad) { + startPageloadSpan(); + } }; } @@ -109,7 +171,7 @@ export function withSentry

, R extends React.Co ): R { const SentryRoot: React.FC

= (props: P) => { // Early return when any of the required functions is not available. - if (!_useEffect || !_useLocation || !_useMatches || !_customStartTransaction) { + if (!_useEffect || !_useLocation || !_useMatches) { DEBUG_BUILD && !isNodeEnv() && logger.warn('Remix SDK was unable to wrap your root because of one or more missing parameters.'); @@ -125,37 +187,37 @@ export function withSentry

, R extends React.Co const matches = _useMatches(); _useEffect(() => { - if (activeTransaction && matches && matches.length) { - activeTransaction.updateName(matches[matches.length - 1].id); - activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + const activeRootSpan = getActiveSpan(); + + if (activeRootSpan && matches && matches.length) { + const transaction = getRootSpan(activeRootSpan); + + if (transaction) { + transaction.updateName(matches[matches.length - 1].id); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } } isBaseLocation = true; }, []); _useEffect(() => { + const activeRootSpan = getActiveSpan(); + if (isBaseLocation) { - if (activeTransaction) { - activeTransaction.end(); + if (activeRootSpan) { + activeRootSpan.end(); } return; } - if (_startTransactionOnLocationChange && matches && matches.length) { - if (activeTransaction) { - activeTransaction.end(); + if (_instrumentNavigation && matches && matches.length) { + if (activeRootSpan) { + activeRootSpan.end(); } - activeTransaction = _customStartTransaction({ - name: matches[matches.length - 1].id, - op: 'navigation', - origin: 'auto.navigation.remix', - tags: DEFAULT_TAGS, - metadata: { - source: 'route', - }, - }); + startNavigationSpan(matches); } }, [location]); @@ -175,3 +237,23 @@ export function withSentry

, R extends React.Co // will break advanced type inference done by react router params return SentryRoot; } + +export function setGlobals({ + useEffect, + 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.client.tsx b/packages/remix/src/index.client.tsx index 9c619ff1d851..3842e9a3701d 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -1,16 +1,26 @@ import { applySdkMetadata } from '@sentry/core'; import { getCurrentScope, init as reactInit } from '@sentry/react'; - import type { RemixOptions } from './utils/remixOptions'; -export { remixRouterInstrumentation, withSentry } from './client/performance'; export { captureRemixErrorBoundaryError } from './client/errors'; +export { + // eslint-disable-next-line deprecation/deprecation + remixRouterInstrumentation, + withSentry, +} from './client/performance'; + +export { browserTracingIntegration } from './client/browserTracingIntegration'; + export * from '@sentry/react'; export function init(options: RemixOptions): void { - applySdkMetadata(options, 'remix', ['remix', 'react']); - options.environment = options.environment || process.env.NODE_ENV; + const opts = { + ...options, + environment: options.environment || process.env.NODE_ENV, + }; + + applySdkMetadata(opts, 'remix', ['remix', 'react']); - reactInit(options); + reactInit(opts); getCurrentScope().setTag('runtime', 'browser'); } diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 674beed61ee6..a34250100287 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -111,8 +111,10 @@ export * from '@sentry/node'; export { captureRemixServerException, wrapRemixHandleError } from './utils/instrumentServer'; export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; +// eslint-disable-next-line deprecation/deprecation export { remixRouterInstrumentation, withSentry } from './client/performance'; export { captureRemixErrorBoundaryError } from './client/errors'; +export { browserTracingIntegration } from './client/browserTracingIntegration'; export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express'; export type { SentryMetaArgs } from './utils/types'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/entry.client.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/entry.client.tsx new file mode 100644 index 000000000000..7273433127ac --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/entry.client.tsx @@ -0,0 +1,12 @@ +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { useEffect } from 'react'; +import { hydrate } from 'react-dom'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [Sentry.browserTracingIntegration({ useEffect, useLocation, useMatches })], +}); + +hydrate(, document); diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/entry.server.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/entry.server.tsx new file mode 100644 index 000000000000..bba366801092 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/entry.server.tsx @@ -0,0 +1,30 @@ +import type { EntryContext } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { renderToString } from 'react-dom/server'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + tracePropagationTargets: ['example.org'], + // Disabling to test series of envelopes deterministically. + autoSessionTracking: false, +}); + +export const handleError = Sentry.wrapRemixHandleError; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + let markup = renderToString(); + + responseHeaders.set('Content-Type', 'text/html'); + + return new Response('' + markup, { + status: responseStatusCode, + headers: responseHeaders, + }); +} diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/root.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/root.tsx new file mode 100644 index 000000000000..15b78b8a6325 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/root.tsx @@ -0,0 +1,73 @@ +import { LoaderFunction, V2_MetaFunction, defer, json, redirect } from '@remix-run/node'; +import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useRouteError } from '@remix-run/react'; +import { V2_ErrorBoundaryComponent } from '@remix-run/react/dist/routeModules'; +import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix'; + +export const ErrorBoundary: V2_ErrorBoundaryComponent = () => { + const error = useRouteError(); + + captureRemixErrorBoundaryError(error); + + return

error
; +}; + +export const meta: V2_MetaFunction = ({ data }) => [ + { charset: 'utf-8' }, + { title: 'New Remix App' }, + { name: 'viewport', content: 'width=device-width,initial-scale=1' }, + { name: 'sentry-trace', content: data.sentryTrace }, + { name: 'baggage', content: data.sentryBaggage }, +]; + +export const loader: LoaderFunction = async ({ request }) => { + const url = new URL(request.url); + const type = url.searchParams.get('type'); + + switch (type) { + case 'empty': + return {}; + case 'plain': + return { + data_one: [], + data_two: 'a string', + }; + case 'json': + return json({ data_one: [], data_two: 'a string' }, { headers: { 'Cache-Control': 'max-age=300' } }); + case 'defer': + return defer({ data_one: [], data_two: 'a string' }); + case 'null': + return null; + case 'undefined': + return undefined; + case 'throwRedirect': + throw redirect('/?type=plain'); + case 'returnRedirect': + return redirect('/?type=plain'); + case 'throwRedirectToExternal': + throw redirect('https://example.com'); + case 'returnRedirectToExternal': + return redirect('https://example.com'); + default: { + return {}; + } + } +}; + +function App() { + return ( + + + + + + + + + + + + + ); +} + +export default withSentry(App); diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/action-json-response.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/action-json-response.$id.tsx new file mode 100644 index 000000000000..7a00bfb2bfe7 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/action-json-response.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/action-json-response.$id'; +export { default } from '../../common/routes/action-json-response.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-exception.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-exception.tsx new file mode 100644 index 000000000000..1ba745d2e63d --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-exception.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/capture-exception'; +export { default } from '../../common/routes/capture-exception'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-message.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-message.tsx new file mode 100644 index 000000000000..9dae2318cc14 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/capture-message.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/capture-message'; +export { default } from '../../common/routes/capture-message'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/error-boundary-capture.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/error-boundary-capture.$id.tsx new file mode 100644 index 000000000000..011f92462069 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/error-boundary-capture.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/error-boundary-capture.$id'; +export { default } from '../../common/routes/error-boundary-capture.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/index.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/index.tsx new file mode 100644 index 000000000000..22c086a4c2cf --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/index.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/index'; +export { default } from '../../common/routes/index'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-defer-response.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-defer-response.$id.tsx new file mode 100644 index 000000000000..69499e594ccc --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-defer-response.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/loader-defer-response.$id'; +export { default } from '../../common/routes/loader-defer-response.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-json-response.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-json-response.$id.tsx new file mode 100644 index 000000000000..7761875bdb76 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-json-response.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/loader-json-response.$id'; +export { default } from '../../common/routes/loader-json-response.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-throw-response.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-throw-response.$id.tsx new file mode 100644 index 000000000000..6b9a6a85cbef --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/loader-throw-response.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/loader-throw-response.$id'; +export { default } from '../../common/routes/loader-throw-response.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/manual-tracing.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/manual-tracing.$id.tsx new file mode 100644 index 000000000000..a7cfebe4ed46 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/manual-tracing.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/manual-tracing.$id'; +export { default } from '../../common/routes/manual-tracing.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/scope-bleed.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/scope-bleed.$id.tsx new file mode 100644 index 000000000000..5ba2376f0339 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/scope-bleed.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/scope-bleed.$id'; +export { default } from '../../common/routes/scope-bleed.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/server-side-unexpected-errors.$id.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/server-side-unexpected-errors.$id.tsx new file mode 100644 index 000000000000..d9571c68ddd5 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/server-side-unexpected-errors.$id.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/server-side-unexpected-errors.$id'; +export { default } from '../../common/routes/server-side-unexpected-errors.$id'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/ssr-error.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/ssr-error.tsx new file mode 100644 index 000000000000..627f7e126871 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/ssr-error.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/ssr-error'; +export { default } from '../../common/routes/ssr-error'; diff --git a/packages/remix/test/integration/app_v2_tracingIntegration/routes/throw-redirect.tsx b/packages/remix/test/integration/app_v2_tracingIntegration/routes/throw-redirect.tsx new file mode 100644 index 000000000000..4425f3432b58 --- /dev/null +++ b/packages/remix/test/integration/app_v2_tracingIntegration/routes/throw-redirect.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/throw-redirect'; +export { default } from '../../common/routes/throw-redirect'; diff --git a/packages/remix/test/integration/remix.config.js b/packages/remix/test/integration/remix.config.js index b4c7ac0837b8..418d3690f696 100644 --- a/packages/remix/test/integration/remix.config.js +++ b/packages/remix/test/integration/remix.config.js @@ -1,8 +1,9 @@ /** @type {import('@remix-run/dev').AppConfig} */ const useV2 = process.env.REMIX_VERSION === '2'; +const useBrowserTracing = process.env.TRACING_INTEGRATION === 'true'; module.exports = { - appDirectory: useV2 ? 'app_v2' : 'app_v1', + appDirectory: useBrowserTracing ? 'app_v2_tracingIntegration' : useV2 ? 'app_v2' : 'app_v1', assetsBuildDirectory: 'public/build', serverBuildPath: 'build/index.js', publicPath: '/build/', From 300bba4862534978165f68b882ec10ed9604fec6 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 5 Feb 2024 15:40:42 +0100 Subject: [PATCH 49/68] ref(nextjs): Remove internally used deprecated APIs (#10453) --- .../src/common/utils/commonObjectTracing.ts | 4 + .../nextjs/src/common/utils/responseEnd.ts | 20 +- .../nextjs/src/common/utils/wrapperUtils.ts | 228 +++++++----------- .../common/withServerActionInstrumentation.ts | 133 +++++----- .../wrapAppGetInitialPropsWithSentry.ts | 20 +- .../wrapErrorGetInitialPropsWithSentry.ts | 20 +- .../wrapGenerationFunctionWithSentry.ts | 48 ++-- .../common/wrapGetInitialPropsWithSentry.ts | 20 +- .../wrapGetServerSidePropsWithSentry.ts | 13 +- .../src/common/wrapRouteHandlerWithSentry.ts | 111 +++++---- .../common/wrapServerComponentWithSentry.ts | 51 ++-- packages/nextjs/test/config/wrappers.test.ts | 94 ++++---- 12 files changed, 336 insertions(+), 426 deletions(-) diff --git a/packages/nextjs/src/common/utils/commonObjectTracing.ts b/packages/nextjs/src/common/utils/commonObjectTracing.ts index bb5cf130bab1..988dee391dc4 100644 --- a/packages/nextjs/src/common/utils/commonObjectTracing.ts +++ b/packages/nextjs/src/common/utils/commonObjectTracing.ts @@ -4,6 +4,10 @@ const commonMap = new WeakMap(); /** * Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context. + * + * @param commonObject The shared object. + * @param propagationContext The propagation context that should be shared between all the resources if no propagation context was registered yet. + * @returns the shared propagation context. */ export function commonObjectToPropagationContext( commonObject: unknown, diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index c12d19f1c6fa..501208f81e84 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -1,13 +1,13 @@ import type { ServerResponse } from 'http'; import { flush, setHttpStatus } from '@sentry/core'; -import type { Transaction } from '@sentry/types'; +import type { Span } from '@sentry/types'; import { fill, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; /** - * Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish. + * Wrap `res.end()` so that it ends the span and flushes events before letting the request finish. * * Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping * things in the right order, in this case it's safe, because the native `.end()` actually *is* (effectively) async, and @@ -20,13 +20,13 @@ import type { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; * `end` doesn't delay data getting to the end user. See * https://nodejs.org/api/http.html#responseenddata-encoding-callback. * - * @param transaction The transaction tracing request handling + * @param span The span tracking the request * @param res: The request's corresponding response */ -export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerResponse): void { +export function autoEndSpanOnResponseEnd(span: Span, res: ServerResponse): void { const wrapEndMethod = (origEnd: ResponseEndMethod): WrappedResponseEndMethod => { return function sentryWrappedEnd(this: ServerResponse, ...args: unknown[]) { - finishTransaction(transaction, this); + finishSpan(span, this); return origEnd.call(this, ...args); }; }; @@ -38,11 +38,11 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S } } -/** Finish the given response's transaction and set HTTP status data */ -export function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): void { - if (transaction) { - setHttpStatus(transaction, res.statusCode); - transaction.end(); +/** Finish the given response's span and set HTTP status data */ +export function finishSpan(span: Span | undefined, res: ServerResponse): void { + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); } } diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 2b9d58d41616..20f458bf6013 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -1,38 +1,40 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, - getActiveSpan, - getActiveTransaction, - getCurrentScope, - runWithAsyncContext, - startTransaction, + continueTrace, + startInactiveSpan, + startSpan, + startSpanManual, + withActiveSpan, + withIsolationScope, } from '@sentry/core'; -import type { Span, Transaction } from '@sentry/types'; -import { isString, tracingContextFromHeaders } from '@sentry/utils'; +import type { Span } from '@sentry/types'; +import { isString } from '@sentry/utils'; import { platformSupportsStreaming } from './platformSupportsStreaming'; -import { autoEndTransactionOnResponseEnd, flushQueue } from './responseEnd'; +import { autoEndSpanOnResponseEnd, flushQueue } from './responseEnd'; declare module 'http' { interface IncomingMessage { - _sentryTransaction?: Transaction; + _sentrySpan?: Span; } } /** - * Grabs a transaction off a Next.js datafetcher request object, if it was previously put there via - * `setTransactionOnRequest`. + * Grabs a span off a Next.js datafetcher request object, if it was previously put there via + * `setSpanOnRequest`. * * @param req The Next.js datafetcher request object - * @returns the Transaction on the request object if there is one, or `undefined` if the request object didn't have one. + * @returns the span on the request object if there is one, or `undefined` if the request object didn't have one. */ -export function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined { - return req._sentryTransaction; +export function getSpanFromRequest(req: IncomingMessage): Span | undefined { + return req._sentrySpan; } -function setTransactionOnRequest(transaction: Transaction, req: IncomingMessage): void { - req._sentryTransaction = transaction; +function setSpanOnRequest(transaction: Span, req: IncomingMessage): void { + req._sentrySpan = transaction; } /** @@ -85,99 +87,68 @@ export function withTracedServerSideDataFetcher Pr }, ): (...params: Parameters) => Promise> { return async function (this: unknown, ...args: Parameters): Promise> { - return runWithAsyncContext(async () => { - const scope = getCurrentScope(); - const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? getActiveSpan(); - let dataFetcherSpan; + return withIsolationScope(async isolationScope => { + isolationScope.setSDKProcessingMetadata({ + request: req, + }); const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; const baggage = req.headers?.baggage; - // eslint-disable-next-line deprecation/deprecation - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTrace, - baggage, - ); - scope.setPropagationContext(propagationContext); - if (platformSupportsStreaming()) { - let spanToContinue: Span; - if (previousSpan === undefined) { - // TODO: Refactor this to use `startSpan()` - // eslint-disable-next-line deprecation/deprecation - const newTransaction = startTransaction( + return continueTrace({ sentryTrace, baggage }, () => { + let requestSpan: Span | undefined = getSpanFromRequest(req); + if (!requestSpan) { + // TODO(v8): Simplify these checks when startInactiveSpan always returns a span + requestSpan = startInactiveSpan({ + name: options.requestedRouteName, + op: 'http.server', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + }); + if (requestSpan) { + requestSpan.setStatus('ok'); + setSpanOnRequest(requestSpan, req); + autoEndSpanOnResponseEnd(requestSpan, res); + } + } + + const withActiveSpanCallback = (): Promise> => { + return startSpanManual( { - op: 'http.server', - name: options.requestedRouteName, - origin: 'auto.function.nextjs', - ...traceparentData, - status: 'ok', - metadata: { - request: req, - source: 'route', - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + op: 'function.nextjs', + name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, }, - { request: req }, + async dataFetcherSpan => { + dataFetcherSpan?.setStatus('ok'); + try { + return await origDataFetcher.apply(this, args); + } catch (e) { + dataFetcherSpan?.setStatus('internal_error'); + requestSpan?.setStatus('internal_error'); + throw e; + } finally { + dataFetcherSpan?.end(); + if (!platformSupportsStreaming()) { + await flushQueue(); + } + } + }, ); + }; - if (platformSupportsStreaming()) { - // On platforms that don't support streaming, doing things after res.end() is unreliable. - autoEndTransactionOnResponseEnd(newTransaction, res); - } - - // Link the transaction and the request together, so that when we would normally only have access to one, it's - // still possible to grab the other. - setTransactionOnRequest(newTransaction, req); - spanToContinue = newTransaction; + if (requestSpan) { + return withActiveSpan(requestSpan, withActiveSpanCallback); } else { - spanToContinue = previousSpan; - } - - // eslint-disable-next-line deprecation/deprecation - dataFetcherSpan = spanToContinue.startChild({ - op: 'function.nextjs', - description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, - origin: 'auto.function.nextjs', - status: 'ok', - }); - } else { - // TODO: Refactor this to use `startSpan()` - // eslint-disable-next-line deprecation/deprecation - dataFetcherSpan = startTransaction({ - op: 'function.nextjs', - name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, - origin: 'auto.function.nextjs', - ...traceparentData, - status: 'ok', - metadata: { - request: req, - source: 'route', - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - }, - }); - } - - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(dataFetcherSpan); - scope.setSDKProcessingMetadata({ request: req }); - - try { - return await origDataFetcher.apply(this, args); - } catch (e) { - // Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation` - // that set the transaction status, we need to manually set the status of the span & transaction - dataFetcherSpan.setStatus('internal_error'); - previousSpan?.setStatus('internal_error'); - throw e; - } finally { - dataFetcherSpan.end(); - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(previousSpan); - if (!platformSupportsStreaming()) { - await flushQueue(); + return withActiveSpanCallback(); } - } + }); }); }; } @@ -199,43 +170,30 @@ export async function callDataFetcherTraced Promis ): Promise> { const { parameterizedRoute, dataFetchingMethodName } = options; - // eslint-disable-next-line deprecation/deprecation - const transaction = getActiveTransaction(); - - if (!transaction) { - return origFunction(...origFunctionArgs); - } - - // TODO: Make sure that the given route matches the name of the active transaction (to prevent background data - // fetching from switching the name to a completely other route) -- We'll probably switch to creating a transaction - // right here so making that check will probabably not even be necessary. - // Logic will be: If there is no active transaction, start one with correct name and source. If there is an active - // transaction, create a child span with correct name and source. - transaction.updateName(parameterizedRoute); - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + return startSpan( + { + op: 'function.nextjs', + name: `${dataFetchingMethodName} (${parameterizedRoute})`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + }, + async dataFetcherSpan => { + dataFetcherSpan?.setStatus('ok'); - // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another - // route's transaction - // eslint-disable-next-line deprecation/deprecation - const span = transaction.startChild({ - op: 'function.nextjs', - origin: 'auto.function.nextjs', - description: `${dataFetchingMethodName} (${parameterizedRoute})`, - status: 'ok', - }); - - try { - return await origFunction(...origFunctionArgs); - } catch (err) { - // Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation` - // that set the transaction status, we need to manually set the status of the span & transaction - transaction.setStatus('internal_error'); - span.setStatus('internal_error'); - span.end(); - - // TODO Copy more robust error handling over from `withSentry` - captureException(err, { mechanism: { handled: false } }); - - throw err; - } + try { + return await origFunction(...origFunctionArgs); + } catch (e) { + dataFetcherSpan?.setStatus('internal_error'); + captureException(e, { mechanism: { handled: false } }); + throw e; + } finally { + dataFetcherSpan?.end(); + if (!platformSupportsStreaming()) { + await flushQueue(); + } + } + }, + ); } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 2fe1fd714b96..2de1497b2dfd 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -1,13 +1,13 @@ import { addTracingExtensions, captureException, + continueTrace, getClient, - getCurrentScope, handleCallbackErrors, - runWithAsyncContext, startSpan, + withIsolationScope, } from '@sentry/core'; -import { logger, tracingContextFromHeaders } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; @@ -57,7 +57,7 @@ async function withServerActionInstrumentationImplementation
> { addTracingExtensions(); - return runWithAsyncContext(async () => { + return withIsolationScope(isolationScope => { const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; let sentryTraceHeader; @@ -76,76 +76,73 @@ async function withServerActionInstrumentationImplementation { + try { + return await startSpan( + { + op: 'function.server_action', + name: `serverAction/${serverActionName}`, + metadata: { + source: 'route', + }, }, - }, - }, - async span => { - const result = await handleCallbackErrors(callback, error => { - if (isNotFoundNavigationError(error)) { - // We don't want to report "not-found"s - span?.setStatus('not_found'); - } else if (isRedirectNavigationError(error)) { - // Don't do anything for redirects - } else { - span?.setStatus('internal_error'); - captureException(error, { - mechanism: { - handled: false, - }, + async span => { + const result = await handleCallbackErrors(callback, error => { + if (isNotFoundNavigationError(error)) { + // We don't want to report "not-found"s + span?.setStatus('not_found'); + } else if (isRedirectNavigationError(error)) { + // Don't do anything for redirects + } else { + span?.setStatus('internal_error'); + captureException(error, { + mechanism: { + handled: false, + }, + }); + } }); - } - }); - if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) { - span?.setAttribute('server_action_result', result); - } - - if (options.formData) { - options.formData.forEach((value, key) => { - span?.setAttribute( - `server_action_form_data.${key}`, - typeof value === 'string' ? value : '[non-string value]', - ); - }); - } + if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) { + isolationScope.setExtra('server_action_result', result); + } - return result; - }, - ); - } finally { - if (!platformSupportsStreaming()) { - // Lambdas require manual flushing to prevent execution freeze before the event is sent - await flushQueue(); - } + if (options.formData) { + options.formData.forEach((value, key) => { + isolationScope.setExtra( + `server_action_form_data.${key}`, + typeof value === 'string' ? value : '[non-string value]', + ); + }); + } - if (process.env.NEXT_RUNTIME === 'edge') { - // flushQueue should not throw - // eslint-disable-next-line @typescript-eslint/no-floating-promises - flushQueue(); - } - } + return result; + }, + ); + } finally { + if (!platformSupportsStreaming()) { + // Lambdas require manual flushing to prevent execution freeze before the event is sent + await flushQueue(); + } - return res; + if (process.env.NEXT_RUNTIME === 'edge') { + // flushQueue should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + flushQueue(); + } + } + }, + ); }); } diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index df18b2ad952d..218ed18b5f26 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -1,19 +1,16 @@ import { addTracingExtensions, + getActiveSpan, getClient, - getCurrentScope, getDynamicSamplingContextFromSpan, + getRootSpan, spanToTraceHeader, } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type App from 'next/app'; import { isBuild } from './utils/isBuild'; -import { - getTransactionFromRequest, - withErrorInstrumentation, - withTracedServerSideDataFetcher, -} from './utils/wrapperUtils'; +import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; type AppGetInitialProps = (typeof App)['getInitialProps']; @@ -58,8 +55,8 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI }; } = await tracedGetInitialProps.apply(thisArg, args); - // eslint-disable-next-line deprecation/deprecation - const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); + const activeSpan = getActiveSpan(); + const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined); // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per @@ -69,10 +66,9 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI appGetInitialProps.pageProps = {}; } - if (requestTransaction) { - appGetInitialProps.pageProps._sentryTraceData = spanToTraceHeader(requestTransaction); - - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); + if (requestSpan) { + appGetInitialProps.pageProps._sentryTraceData = spanToTraceHeader(requestSpan); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); appGetInitialProps.pageProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index 44a171d8e6d5..2b2ad24fd18e 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -1,8 +1,9 @@ import { addTracingExtensions, + getActiveSpan, getClient, - getCurrentScope, getDynamicSamplingContextFromSpan, + getRootSpan, spanToTraceHeader, } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; @@ -10,11 +11,7 @@ import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; import { isBuild } from './utils/isBuild'; -import { - getTransactionFromRequest, - withErrorInstrumentation, - withTracedServerSideDataFetcher, -} from './utils/wrapperUtils'; +import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; type ErrorGetInitialProps = (context: NextPageContext) => Promise; @@ -59,12 +56,13 @@ export function wrapErrorGetInitialPropsWithSentry( _sentryBaggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); - // eslint-disable-next-line deprecation/deprecation - const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); - if (requestTransaction) { - errorGetInitialProps._sentryTraceData = spanToTraceHeader(requestTransaction); + const activeSpan = getActiveSpan(); + const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined); + + if (requestSpan) { + errorGetInitialProps._sentryTraceData = spanToTraceHeader(requestSpan); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); errorGetInitialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index ec4fe9048900..276cbec81d35 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -2,15 +2,13 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, - continueTrace, getClient, - getCurrentScope, handleCallbackErrors, - runWithAsyncContext, startSpanManual, + withIsolationScope, } from '@sentry/core'; import type { WebFetchHeaders } from '@sentry/types'; -import { winterCGHeadersToDict } from '@sentry/utils'; +import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; @@ -46,42 +44,31 @@ export function wrapGenerationFunctionWithSentry a data = { params, searchParams }; } - return runWithAsyncContext(() => { - // eslint-disable-next-line deprecation/deprecation - const transactionContext = continueTrace({ - baggage: headers?.get('baggage'), - sentryTrace: headers?.get('sentry-trace') ?? undefined, + return withIsolationScope(isolationScope => { + isolationScope.setSDKProcessingMetadata({ + request: { + headers: headers ? winterCGHeadersToDict(headers) : undefined, + }, }); + isolationScope.setExtra('route_data', data); + + const incomingPropagationContext = propagationContextFromHeaders( + headers?.get('sentry-trace') ?? undefined, + headers?.get('baggage'), + ); - // If there is no incoming trace, we are setting the transaction context to one that is shared between all other - // transactions for this request. We do this based on the `headers` object, which is the same for all components. - const propagationContext = getCurrentScope().getPropagationContext(); - if (!transactionContext.traceId && !transactionContext.parentSpanId) { - const { traceId: commonTraceId, spanId: commonSpanId } = commonObjectToPropagationContext( - headers, - propagationContext, - ); - transactionContext.traceId = commonTraceId; - transactionContext.parentSpanId = commonSpanId; - } + const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); + isolationScope.setPropagationContext(propagationContext); return startSpanManual( { op: 'function.nextjs', name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, - ...transactionContext, data, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', }, - metadata: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.metadata, - request: { - headers: headers ? winterCGHeadersToDict(headers) : undefined, - }, - }, }, span => { return handleCallbackErrors( @@ -98,9 +85,6 @@ export function wrapGenerationFunctionWithSentry a captureException(err, { mechanism: { handled: false, - data: { - function: 'wrapGenerationFunctionWithSentry', - }, }, }); } diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index 1a6743765cd6..2dbe5c34d6c9 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -1,19 +1,16 @@ import { addTracingExtensions, + getActiveSpan, getClient, - getCurrentScope, getDynamicSamplingContextFromSpan, + getRootSpan, spanToTraceHeader, } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPage } from 'next'; import { isBuild } from './utils/isBuild'; -import { - getTransactionFromRequest, - withErrorInstrumentation, - withTracedServerSideDataFetcher, -} from './utils/wrapperUtils'; +import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; type GetInitialProps = Required['getInitialProps']; @@ -55,12 +52,13 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro _sentryBaggage?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. - // eslint-disable-next-line deprecation/deprecation - const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); - if (requestTransaction) { - initialProps._sentryTraceData = spanToTraceHeader(requestTransaction); + const activeSpan = getActiveSpan(); + const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined); + + if (requestSpan) { + initialProps._sentryTraceData = spanToTraceHeader(requestSpan); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); initialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index 691570f87683..1f21952ec373 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -1,19 +1,16 @@ import { addTracingExtensions, + getActiveSpan, getClient, - getCurrentScope, getDynamicSamplingContextFromSpan, + getRootSpan, spanToTraceHeader, } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { GetServerSideProps } from 'next'; import { isBuild } from './utils/isBuild'; -import { - getTransactionFromRequest, - withErrorInstrumentation, - withTracedServerSideDataFetcher, -} from './utils/wrapperUtils'; +import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; /** * Create a wrapped version of the user's exported `getServerSideProps` function @@ -52,8 +49,8 @@ export function wrapGetServerSidePropsWithSentry( >); if (serverSideProps && 'props' in serverSideProps) { - // eslint-disable-next-line deprecation/deprecation - const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); + const activeSpan = getActiveSpan(); + const requestTransaction = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined); if (requestTransaction) { serverSideProps.props._sentryTraceData = spanToTraceHeader(requestTransaction); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index e6154ad02c89..e4a475f6ced6 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -1,13 +1,15 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, - getCurrentScope, + continueTrace, handleCallbackErrors, - runWithAsyncContext, setHttpStatus, startSpan, + withIsolationScope, } from '@sentry/core'; -import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; +import { winterCGHeadersToDict } from '@sentry/utils'; import { isRedirectNavigationError } from './nextNavigationErrorUtils'; import type { RouteHandlerContext } from './types'; @@ -27,64 +29,61 @@ export function wrapRouteHandlerWithSentry any>( const { method, parameterizedRoute, baggageHeader, sentryTraceHeader, headers } = context; return new Proxy(routeHandler, { apply: (originalFunction, thisArg, args) => { - return runWithAsyncContext(async () => { - // eslint-disable-next-line deprecation/deprecation - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTraceHeader ?? headers?.get('sentry-trace') ?? undefined, - baggageHeader ?? headers?.get('baggage'), - ); - getCurrentScope().setPropagationContext(propagationContext); - - let result; - - try { - result = await startSpan( - { - op: 'http.server', - name: `${method} ${parameterizedRoute}`, - status: 'ok', - ...traceparentData, - metadata: { - request: { - headers: headers ? winterCGHeadersToDict(headers) : undefined, + return withIsolationScope(async isolationScope => { + isolationScope.setSDKProcessingMetadata({ + request: { + headers: headers ? winterCGHeadersToDict(headers) : undefined, + }, + }); + return continueTrace( + { + sentryTrace: sentryTraceHeader ?? headers?.get('sentry-trace') ?? undefined, + baggage: baggageHeader ?? headers?.get('baggage'), + }, + async () => { + try { + return await startSpan( + { + op: 'http.server', + name: `${method} ${parameterizedRoute}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + }, }, - source: 'route', - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - }, - }, - async span => { - const response: Response = await handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - error => { - // Next.js throws errors when calling `redirect()`. We don't wanna report these. - if (!isRedirectNavigationError(error)) { - captureException(error, { - mechanism: { - handled: false, - }, - }); + async span => { + const response: Response = await handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + error => { + // Next.js throws errors when calling `redirect()`. We don't wanna report these. + if (!isRedirectNavigationError(error)) { + captureException(error, { + mechanism: { + handled: false, + }, + }); + } + }, + ); + + try { + span && setHttpStatus(span, response.status); + } catch { + // best effort - response may be undefined? } + + return response; }, ); - - try { - span && setHttpStatus(span, response.status); - } catch { - // best effort + } finally { + if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') { + // 1. Edge transport requires manual flushing + // 2. Lambdas require manual flushing to prevent execution freeze before the event is sent + await flushQueue(); } - - return response; - }, - ); - } finally { - if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') { - // 1. Edge tranpsort requires manual flushing - // 2. Lambdas require manual flushing to prevent execution freeze before the event is sent - await flushQueue(); - } - } - - return result; + } + }, + ); }); }, }); diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 59a608406e09..6d6e7758bbf9 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -2,13 +2,11 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, - continueTrace, - getCurrentScope, handleCallbackErrors, - runWithAsyncContext, startSpanManual, + withIsolationScope, } from '@sentry/core'; -import { winterCGHeadersToDict } from '@sentry/utils'; +import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; @@ -32,48 +30,36 @@ export function wrapServerComponentWithSentry any> // hook. 🤯 return new Proxy(appDirComponent, { apply: (originalFunction, thisArg, args) => { - return runWithAsyncContext(() => { + // TODO: If we ever allow withIsolationScope to take a scope, we should pass a scope here that is shared between all of the server components, similar to what `commonObjectToPropagationContext` does. + return withIsolationScope(isolationScope => { const completeHeadersDict: Record = context.headers ? winterCGHeadersToDict(context.headers) : {}; - // eslint-disable-next-line deprecation/deprecation - const transactionContext = continueTrace({ + isolationScope.setSDKProcessingMetadata({ + request: { + headers: completeHeadersDict, + }, + }); + + const incomingPropagationContext = propagationContextFromHeaders( // eslint-disable-next-line deprecation/deprecation - sentryTrace: context.sentryTraceHeader ?? completeHeadersDict['sentry-trace'], + context.sentryTraceHeader ?? completeHeadersDict['sentry-trace'], // eslint-disable-next-line deprecation/deprecation - baggage: context.baggageHeader ?? completeHeadersDict['baggage'], - }); + context.baggageHeader ?? completeHeadersDict['baggage'], + ); - // If there is no incoming trace, we are setting the transaction context to one that is shared between all other - // transactions for this request. We do this based on the `headers` object, which is the same for all components. - const propagationContext = getCurrentScope().getPropagationContext(); - if (!transactionContext.traceId && !transactionContext.parentSpanId) { - const { traceId: commonTraceId, spanId: commonSpanId } = commonObjectToPropagationContext( - context.headers, - propagationContext, - ); - transactionContext.traceId = commonTraceId; - transactionContext.parentSpanId = commonSpanId; - } + const propagationContext = commonObjectToPropagationContext(context.headers, incomingPropagationContext); + isolationScope.setPropagationContext(propagationContext); - const res = startSpanManual( + return startSpanManual( { - ...transactionContext, op: 'function.nextjs', name: `${componentType} Server Component (${componentRoute})`, - status: 'ok', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', }, - metadata: { - // eslint-disable-next-line deprecation/deprecation - ...transactionContext.metadata, - request: { - headers: completeHeadersDict, - }, - }, }, span => { return handleCallbackErrors( @@ -87,7 +73,6 @@ export function wrapServerComponentWithSentry any> span?.setStatus('ok'); } else { span?.setStatus('internal_error'); - captureException(error, { mechanism: { handled: false, @@ -105,8 +90,6 @@ export function wrapServerComponentWithSentry any> ); }, ); - - return res; }); }, }); diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index 95b003e4e14d..b15af158a098 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -1,77 +1,73 @@ import type { IncomingMessage, ServerResponse } from 'http'; import * as SentryCore from '@sentry/core'; -import { addTracingExtensions } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; import type { Client } from '@sentry/types'; import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common'; -const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction'); +const startSpanManualSpy = jest.spyOn(SentryCore, 'startSpanManual'); // The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient // constructor but the client isn't used in these tests. addTracingExtensions(); -describe('data-fetching function wrappers', () => { +describe('data-fetching function wrappers should create spans', () => { const route = '/tricks/[trickName]'; let req: IncomingMessage; let res: ServerResponse; - describe('starts a transaction and puts request in metadata if tracing enabled', () => { - beforeEach(() => { - req = { headers: {}, url: 'http://dogs.are.great/tricks/kangaroo' } as IncomingMessage; - res = { end: jest.fn() } as unknown as ServerResponse; + beforeEach(() => { + req = { headers: {}, url: 'http://dogs.are.great/tricks/kangaroo' } as IncomingMessage; + res = { end: jest.fn() } as unknown as ServerResponse; - jest.spyOn(SentryCore, 'hasTracingEnabled').mockReturnValue(true); - jest.spyOn(SentryCore, 'getClient').mockImplementation(() => { - return { - getOptions: () => ({ instrumenter: 'sentry' }), - getDsn: () => {}, - } as Client; - }); + jest.spyOn(SentryCore, 'hasTracingEnabled').mockReturnValue(true); + jest.spyOn(SentryCore, 'getClient').mockImplementation(() => { + return { + getOptions: () => ({ instrumenter: 'sentry' }), + getDsn: () => {}, + } as Client; }); + }); - afterEach(() => { - jest.clearAllMocks(); - }); + afterEach(() => { + jest.clearAllMocks(); + }); - test('wrapGetServerSidePropsWithSentry', async () => { - const origFunction = jest.fn(async () => ({ props: {} })); + test('wrapGetServerSidePropsWithSentry', async () => { + const origFunction = jest.fn(async () => ({ props: {} })); - const wrappedOriginal = wrapGetServerSidePropsWithSentry(origFunction, route); - await wrappedOriginal({ req, res } as any); + const wrappedOriginal = wrapGetServerSidePropsWithSentry(origFunction, route); + await wrappedOriginal({ req, res } as any); - expect(startTransactionSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: '/tricks/[trickName]', - op: 'http.server', - metadata: expect.objectContaining({ source: 'route', request: req }), - }), - { - request: expect.objectContaining({ - url: 'http://dogs.are.great/tricks/kangaroo', - }), + expect(startSpanManualSpy).toHaveBeenCalledWith( + { + name: 'getServerSideProps (/tricks/[trickName])', + op: 'function.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - ); - }); + }, + expect.any(Function), + ); + }); - test('wrapGetInitialPropsWithSentry', async () => { - const origFunction = jest.fn(async () => ({})); + test('wrapGetInitialPropsWithSentry', async () => { + const origFunction = jest.fn(async () => ({})); - const wrappedOriginal = wrapGetInitialPropsWithSentry(origFunction); - await wrappedOriginal({ req, res, pathname: route } as any); + const wrappedOriginal = wrapGetInitialPropsWithSentry(origFunction); + await wrappedOriginal({ req, res, pathname: route } as any); - expect(startTransactionSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: '/tricks/[trickName]', - op: 'http.server', - metadata: expect.objectContaining({ source: 'route', request: req }), - }), - { - request: expect.objectContaining({ - url: 'http://dogs.are.great/tricks/kangaroo', - }), + expect(startSpanManualSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'getInitialProps (/tricks/[trickName])', + op: 'function.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - ); - }); + }), + expect.any(Function), + ); }); }); From 94cdd8bda146455b7b4a46ed52a233b9394bd953 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 5 Feb 2024 10:43:14 -0400 Subject: [PATCH 50/68] feat(core): Add metric summaries to spans (#10432) --- .../tracing/metric-summaries/scenario.js | 48 ++++++++++ .../suites/tracing/metric-summaries/test.ts | 69 +++++++++++++++ packages/core/src/metrics/aggregator.ts | 11 ++- .../core/src/metrics/browser-aggregator.ts | 27 +++--- packages/core/src/metrics/metric-summary.ts | 87 +++++++++++++++++++ packages/core/src/tracing/span.ts | 2 + packages/core/src/tracing/transaction.ts | 2 + packages/types/src/event.ts | 3 +- packages/types/src/index.ts | 7 +- packages/types/src/span.ts | 9 ++ 10 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js create mode 100644 dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts create mode 100644 packages/core/src/metrics/metric-summary.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 new file mode 100644 index 000000000000..8a7dbabe0dec --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js @@ -0,0 +1,48 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + _experiments: { + metricsAggregator: true, + }, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + () => { + Sentry.metrics.increment('root-counter'); + Sentry.metrics.increment('root-counter'); + + Sentry.startSpan( + { + name: 'Some other span', + op: 'transaction', + }, + () => { + Sentry.metrics.increment('root-counter'); + Sentry.metrics.increment('root-counter'); + Sentry.metrics.increment('root-counter', 2); + + Sentry.metrics.set('root-set', 'some-value'); + Sentry.metrics.set('root-set', 'another-value'); + Sentry.metrics.set('root-set', 'another-value'); + + Sentry.metrics.gauge('root-gauge', 42); + Sentry.metrics.gauge('root-gauge', 20); + + Sentry.metrics.distribution('root-distribution', 42); + Sentry.metrics.distribution('root-distribution', 20); + }, + ); + }, +); diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts new file mode 100644 index 000000000000..98ed58a75c57 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts @@ -0,0 +1,69 @@ +import { createRunner } from '../../../utils/runner'; + +const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + _metrics_summary: { + 'c:root-counter@none': { + min: 1, + max: 1, + count: 2, + sum: 2, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + }, + spans: expect.arrayContaining([ + expect.objectContaining({ + description: 'Some other span', + op: 'transaction', + _metrics_summary: { + 'c:root-counter@none': { + min: 1, + max: 2, + count: 3, + sum: 4, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + 's:root-set@none': { + min: 0, + max: 1, + count: 3, + sum: 2, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + 'g:root-gauge@none': { + min: 20, + max: 42, + count: 2, + sum: 62, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + 'd:root-distribution@none': { + min: 20, + max: 42, + count: 2, + sum: 62, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + }, + }), + ]), +}; + +test('Should add metric summaries to spans', done => { + createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); +}); diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index 6a49fda5918b..2b331082ab3e 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -6,8 +6,9 @@ import type { Primitive, } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; +import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; import { METRIC_MAP } from './instance'; +import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -62,7 +63,11 @@ export class MetricsAggregator implements MetricsAggregatorBase { const tags = sanitizeTags(unsanitizedTags); const bucketKey = getBucketKey(metricType, name, unit, tags); + let bucketItem = this._buckets.get(bucketKey); + // If this is a set metric, we need to calculate the delta from the previous weight. + const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; + if (bucketItem) { bucketItem.metric.add(value); // TODO(abhi): Do we need this check? @@ -82,6 +87,10 @@ export class MetricsAggregator implements MetricsAggregatorBase { this._buckets.set(bucketKey, bucketItem); } + // If value is a string, it's a set metric so calculate the delta from the previous weight. + const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; + updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); + // We need to keep track of the total weight of the buckets so that we can // flush them when we exceed the max weight. this._bucketsTotalWeight += bucketItem.metric.weight; diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts index 5b5c81353024..40cfa1d404ab 100644 --- a/packages/core/src/metrics/browser-aggregator.ts +++ b/packages/core/src/metrics/browser-aggregator.ts @@ -1,14 +1,8 @@ -import type { - Client, - ClientOptions, - MeasurementUnit, - MetricBucketItem, - MetricsAggregator, - Primitive, -} from '@sentry/types'; +import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; +import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; import { METRIC_MAP } from './instance'; +import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -46,7 +40,11 @@ export class BrowserMetricsAggregator implements MetricsAggregator { const tags = sanitizeTags(unsanitizedTags); const bucketKey = getBucketKey(metricType, name, unit, tags); - const bucketItem: MetricBucketItem | undefined = this._buckets.get(bucketKey); + + let bucketItem = this._buckets.get(bucketKey); + // If this is a set metric, we need to calculate the delta from the previous weight. + const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; + if (bucketItem) { bucketItem.metric.add(value); // TODO(abhi): Do we need this check? @@ -54,7 +52,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator { bucketItem.timestamp = timestamp; } } else { - this._buckets.set(bucketKey, { + bucketItem = { // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. metric: new METRIC_MAP[metricType](value), timestamp, @@ -62,8 +60,13 @@ export class BrowserMetricsAggregator implements MetricsAggregator { name, unit, tags, - }); + }; + this._buckets.set(bucketKey, bucketItem); } + + // If value is a string, it's a set metric so calculate the delta from the previous weight. + const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; + updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); } /** diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts new file mode 100644 index 000000000000..dff610574b2c --- /dev/null +++ b/packages/core/src/metrics/metric-summary.ts @@ -0,0 +1,87 @@ +import type { MeasurementUnit, Span } from '@sentry/types'; +import type { MetricSummary } from '@sentry/types'; +import type { Primitive } from '@sentry/types'; +import { dropUndefinedKeys } from '@sentry/utils'; +import { getActiveSpan } from '../tracing'; +import type { MetricType } from './types'; + +/** + * key: bucketKey + * value: [exportKey, MetricSummary] + */ +type MetricSummaryStorage = Map; + +let SPAN_METRIC_SUMMARY: WeakMap | undefined; + +function getMetricStorageForSpan(span: Span): MetricSummaryStorage | undefined { + return SPAN_METRIC_SUMMARY ? SPAN_METRIC_SUMMARY.get(span) : undefined; +} + +/** + * Fetches the metric summary if it exists for the passed span + */ +export function getMetricSummaryJsonForSpan(span: Span): Record | undefined { + const storage = getMetricStorageForSpan(span); + + if (!storage) { + return undefined; + } + const output: Record = {}; + + for (const [, [exportKey, summary]] of storage) { + output[exportKey] = dropUndefinedKeys(summary); + } + + return output; +} + +/** + * Updates the metric summary on the currently active span + */ +export function updateMetricSummaryOnActiveSpan( + metricType: MetricType, + sanitizedName: string, + value: number, + unit: MeasurementUnit, + tags: Record, + bucketKey: string, +): void { + const span = getActiveSpan(); + if (span) { + const storage = getMetricStorageForSpan(span) || new Map(); + + const exportKey = `${metricType}:${sanitizedName}@${unit}`; + const bucketItem = storage.get(bucketKey); + + if (bucketItem) { + const [, summary] = bucketItem; + storage.set(bucketKey, [ + exportKey, + { + min: Math.min(summary.min, value), + max: Math.max(summary.max, value), + count: (summary.count += 1), + sum: (summary.sum += value), + tags: summary.tags, + }, + ]); + } else { + storage.set(bucketKey, [ + exportKey, + { + min: value, + max: value, + count: 1, + sum: value, + tags, + }, + ]); + } + + if (!SPAN_METRIC_SUMMARY) { + SPAN_METRIC_SUMMARY = new WeakMap(); + } + + SPAN_METRIC_SUMMARY.set(span, storage); + } +} diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 165677455d7f..1e12628ae2a1 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -16,6 +16,7 @@ import type { import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import { getRootSpan } from '../utils/getRootSpan'; import { @@ -624,6 +625,7 @@ export class Span implements SpanInterface { timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + _metrics_summary: getMetricSummaryJsonForSpan(this), }); } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 026723929471..709aa628f42e 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -15,6 +15,7 @@ import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; +import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; @@ -331,6 +332,7 @@ export class Transaction extends SpanClass implements TransactionInterface { capturedSpanIsolationScope, dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, + _metrics_summary: getMetricSummaryJsonForSpan(this), ...(source && { transaction_info: { source, diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 50322f18fbc6..9e16100bbb1b 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -11,7 +11,7 @@ import type { Request } from './request'; import type { CaptureContext } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { Severity, SeverityLevel } from './severity'; -import type { Span, SpanJSON } from './span'; +import type { MetricSummary, Span, SpanJSON } from './span'; import type { Thread } from './thread'; import type { TransactionSource } from './transaction'; import type { User } from './user'; @@ -73,6 +73,7 @@ export interface ErrorEvent extends Event { } export interface TransactionEvent extends Event { type: 'transaction'; + _metrics_summary?: Record; } /** JSDoc */ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5970383febc3..d4fcd439ae4a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -99,6 +99,7 @@ export type { SpanJSON, SpanContextData, TraceFlag, + MetricSummary, } from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; @@ -150,5 +151,9 @@ export type { export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions'; export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin'; -export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics'; +export type { + MetricsAggregator, + MetricBucketItem, + MetricInstance, +} from './metrics'; export type { ParameterizedString } from './parameterize'; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 73c2fbdaaaa8..0743497f1411 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -31,6 +31,14 @@ export type SpanAttributes = Partial<{ }> & Record; +export type MetricSummary = { + min: number; + max: number; + count: number; + sum: number; + tags?: Record | undefined; +}; + /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; @@ -47,6 +55,7 @@ export interface SpanJSON { timestamp?: number; trace_id: string; origin?: SpanOrigin; + _metrics_summary?: Record; } // These are aligned with OpenTelemetry trace flags From 8176f01e146a62153e06d574d079f06c3acfd80c Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 5 Feb 2024 10:27:14 -0500 Subject: [PATCH 51/68] ci(profiling): run compile only if bindings have changed (#10494) Mitigates https://github.com/getsentry/sentry-javascript/issues/10486 by skipping compilation unless binaries have changed or we are on release branch --- .github/workflows/build.yml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f151f9ec33b1..b8977d691322 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,6 +137,8 @@ jobs: - *shared - 'packages/node/**' - 'packages/profiling-node/**' + profiling_node_bindings: + - 'packages/profiling-node/bindings/**' deno: - *shared - *browser @@ -155,6 +157,7 @@ jobs: changed_remix: ${{ steps.changed.outputs.remix }} changed_node: ${{ steps.changed.outputs.node }} changed_profiling_node: ${{ steps.changed.outputs.profiling_node }} + changed_profiling_node_bindings: ${{ steps.changed.outputs.profiling_node_bindings }} changed_deno: ${{ steps.changed.outputs.deno }} changed_browser: ${{ steps.changed.outputs.browser }} changed_browser_integration: ${{ steps.changed.outputs.browser_integration }} @@ -976,13 +979,20 @@ jobs: - name: Build tarballs run: yarn build:tarball --ignore @sentry/profiling-node - # Rebuild profiling by compiling TS and pulling the precompiled binaries + # Rebuild profiling by compiling TS and pull the precompiled binary artifacts - name: Build Profiling Node + if: | + (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (needs.job_get_metadata.outputs.is_release == 'true') || + (github.event_name != 'pull_request') run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries # @TODO: v4 breaks convenient merging of same name artifacts # https://github.com/actions/upload-artifact/issues/478 + if: | + (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (github.event_name != 'pull_request') uses: actions/download-artifact@v3 with: name: profiling-node-binaries-${{ github.sha }} @@ -1086,13 +1096,20 @@ jobs: env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - # Rebuild profiling by compiling TS and pulling the precompiled binaries + # Rebuild profiling by compiling TS and pull the precompiled binary artifacts - name: Build Profiling Node + if: | + (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (needs.job_get_metadata.outputs.is_release == 'true') || + (github.event_name != 'pull_request') run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries # @TODO: v4 breaks convenient merging of same name artifacts # https://github.com/actions/upload-artifact/issues/478 + if: | + (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || + (github.event_name != 'pull_request') uses: actions/download-artifact@v3 with: name: profiling-node-binaries-${{ github.sha }} @@ -1226,9 +1243,9 @@ jobs: name: Compile & Test Profiling Bindings (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.node || matrix.container }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} needs: [job_get_metadata, job_install_deps, job_build] # Compiling bindings can be very slow (especially on windows), so only run precompile - # if profiling or profiling node package had changed or if we are on a release branch. + # Skip precompile unless we are on a release branch as precompile slows down CI times. if: | - (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || + (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') runs-on: ${{ matrix.os }} From ef9196be8caacd475e7f48362591be591c0ef7dc Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 5 Feb 2024 17:37:43 +0100 Subject: [PATCH 52/68] feat(react): Add `reactRouterV4/V5BrowserTracingIntegration` for react router v4 & v5 (#10488) This adds new `reactRouterV4BrowserTracingIntegration()` and `reactRouterV5BrowserTracingIntegration()` exports, deprecating these old routing instrumentations. I opted to leave as much as possible as-is for now, except for streamlining the attributes/tags we use for the instrumentation. Tests lifted from https://github.com/getsentry/sentry-javascript/pull/10430 --- packages/react/src/index.ts | 10 +- packages/react/src/reactrouter.tsx | 168 ++++++-- packages/react/test/reactrouterv4.test.tsx | 428 ++++++++++++++++++-- packages/react/test/reactrouterv5.test.tsx | 434 +++++++++++++++++++-- 4 files changed, 934 insertions(+), 106 deletions(-) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ad66d1e77801..c8be42b00d3b 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,7 +6,15 @@ export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; export { ErrorBoundary, withErrorBoundary } from './errorboundary'; export { createReduxEnhancer } from './redux'; export { reactRouterV3Instrumentation } from './reactrouterv3'; -export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter'; +export { + // eslint-disable-next-line deprecation/deprecation + reactRouterV4Instrumentation, + // eslint-disable-next-line deprecation/deprecation + reactRouterV5Instrumentation, + withSentryRouting, + reactRouterV4BrowserTracingIntegration, + reactRouterV5BrowserTracingIntegration, +} from './reactrouter'; export { reactRouterV6Instrumentation, withSentryReactRouterV6Routing, diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 04995ee4bc44..ba6fc523ee58 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -1,6 +1,18 @@ -import { WINDOW } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import type { Transaction, TransactionSource } from '@sentry/types'; +import { + WINDOW, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, +} from '@sentry/browser'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + getActiveSpan, + getRootSpan, + spanToJSON, +} from '@sentry/core'; +import type { Integration, Span, StartSpanOptions, Transaction, TransactionSource } from '@sentry/types'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -23,29 +35,121 @@ export type RouteConfig = { routes?: RouteConfig[]; }; -type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null; // eslint-disable-line @typescript-eslint/no-explicit-any +export type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null; // eslint-disable-line @typescript-eslint/no-explicit-any + +interface ReactRouterOptions { + history: RouterHistory; + routes?: RouteConfig[]; + matchPath?: MatchPath; +} let activeTransaction: Transaction | undefined; +/** + * A browser tracing integration that uses React Router v4 to instrument navigations. + * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options. + */ +export function reactRouterV4BrowserTracingIntegration( + options: Parameters[0] & ReactRouterOptions, +): Integration { + const integration = browserTracingIntegration({ + ...options, + instrumentPageLoad: false, + instrumentNavigation: false, + }); + + const { history, routes, matchPath, instrumentPageLoad = true, instrumentNavigation = true } = options; + + return { + ...integration, + afterAllSetup(client) { + integration.afterAllSetup(client); + + const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingPageLoadSpan(client, startSpanOptions); + return undefined; + }; + + const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingNavigationSpan(client, startSpanOptions); + return undefined; + }; + + // eslint-disable-next-line deprecation/deprecation + const instrumentation = reactRouterV4Instrumentation(history, routes, matchPath); + + // Now instrument page load & navigation with correct settings + instrumentation(startPageloadCallback, instrumentPageLoad, false); + instrumentation(startNavigationCallback, false, instrumentNavigation); + }, + }; +} + +/** + * A browser tracing integration that uses React Router v5 to instrument navigations. + * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options. + */ +export function reactRouterV5BrowserTracingIntegration( + options: Parameters[0] & ReactRouterOptions, +): Integration { + const integration = browserTracingIntegration({ + ...options, + instrumentPageLoad: false, + instrumentNavigation: false, + }); + + const { history, routes, matchPath } = options; + + return { + ...integration, + afterAllSetup(client) { + integration.afterAllSetup(client); + + const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingPageLoadSpan(client, startSpanOptions); + return undefined; + }; + + const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingNavigationSpan(client, startSpanOptions); + return undefined; + }; + + // eslint-disable-next-line deprecation/deprecation + const instrumentation = reactRouterV5Instrumentation(history, routes, matchPath); + + // Now instrument page load & navigation with correct settings + instrumentation(startPageloadCallback, options.instrumentPageLoad, false); + instrumentation(startNavigationCallback, false, options.instrumentNavigation); + }, + }; +} + +/** + * @deprecated Use `browserTracingReactRouterV4()` instead. + */ export function reactRouterV4Instrumentation( history: RouterHistory, routes?: RouteConfig[], matchPath?: MatchPath, ): ReactRouterInstrumentation { - return createReactRouterInstrumentation(history, 'react-router-v4', routes, matchPath); + return createReactRouterInstrumentation(history, 'reactrouter_v4', routes, matchPath); } +/** + * @deprecated Use `browserTracingReactRouterV5()` instead. + */ export function reactRouterV5Instrumentation( history: RouterHistory, routes?: RouteConfig[], matchPath?: MatchPath, ): ReactRouterInstrumentation { - return createReactRouterInstrumentation(history, 'react-router-v5', routes, matchPath); + return createReactRouterInstrumentation(history, 'reactrouter_v5', routes, matchPath); } function createReactRouterInstrumentation( history: RouterHistory, - name: string, + instrumentationName: string, allRoutes: RouteConfig[] = [], matchPath?: MatchPath, ): ReactRouterInstrumentation { @@ -83,21 +187,17 @@ function createReactRouterInstrumentation( return [pathname, 'url']; } - const tags = { - 'routing.instrumentation': name, - }; - return (customStartTransaction, startTransactionOnPageLoad = true, startTransactionOnLocationChange = true): void => { const initPathName = getInitPathName(); + if (startTransactionOnPageLoad && initPathName) { const [name, source] = normalizeTransactionName(initPathName); activeTransaction = customStartTransaction({ name, - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - tags, - metadata: { - source, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, }, }); } @@ -112,11 +212,10 @@ function createReactRouterInstrumentation( const [name, source] = normalizeTransactionName(location.pathname); activeTransaction = customStartTransaction({ name, - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags, - metadata: { - source, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, }, }); } @@ -164,10 +263,12 @@ function computeRootMatch(pathname: string): Match { export function withSentryRouting

, R extends React.ComponentType

>(Route: R): R { const componentDisplayName = (Route as any).displayName || (Route as any).name; + const activeRootSpan = getActiveRootSpan(); + const WrappedRoute: React.FC

= (props: P) => { - if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) { - activeTransaction.updateName(props.computedMatch.path); - activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + if (activeRootSpan && props && props.computedMatch && props.computedMatch.isExact) { + activeRootSpan.updateName(props.computedMatch.path); + activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } // @ts-expect-error Setting more specific React Component typing for `R` generic above @@ -184,3 +285,22 @@ export function withSentryRouting

, R extends React return WrappedRoute; } /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */ + +function getActiveRootSpan(): Span | undefined { + // Legacy behavior for "old" react router instrumentation + if (activeTransaction) { + return activeTransaction; + } + + const span = getActiveSpan(); + const rootSpan = span ? getRootSpan(span) : undefined; + + if (!rootSpan) { + return undefined; + } + + const op = spanToJSON(rootSpan).op; + + // Only use this root span if it is a pageload or navigation span + return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; +} diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 5849bb688598..973bda75d273 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,14 +1,26 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + createTransport, + getCurrentScope, + setCurrentClient, +} from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Route, Router, Switch, matchPath } from 'react-router-4'; -import { reactRouterV4Instrumentation, withSentryRouting } from '../src'; +import { + BrowserClient, + reactRouterV4BrowserTracingIntegration, + reactRouterV4Instrumentation, + withSentryRouting, +} from '../src'; import type { RouteConfig } from '../src/reactrouter'; -describe('React Router v4', () => { +describe('reactRouterV4Instrumentation', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; @@ -28,6 +40,7 @@ describe('React Router v4', () => { const mockStartTransaction = jest .fn() .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); + // eslint-disable-next-line deprecation/deprecation reactRouterV4Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, @@ -41,10 +54,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }, }); }); @@ -71,10 +85,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); act(() => { @@ -83,10 +98,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/features', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); }); @@ -162,10 +178,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/users/123', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); }); @@ -190,10 +207,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/users/123', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); @@ -221,10 +239,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/1234/v1/758', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); @@ -238,10 +257,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/543', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); expect(mockUpdateName).toHaveBeenCalledTimes(3); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid'); @@ -273,10 +293,11 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid/v1/:teamid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); act(() => { @@ -285,10 +306,339 @@ describe('React Router v4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v4' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); +}); + +const mockStartBrowserTracingPageLoadSpan = jest.fn(); +const mockStartBrowserTracingNavigationSpan = jest.fn(); + +const mockRootSpan = { + updateName: jest.fn(), + setAttribute: jest.fn(), + getSpanJSON() { + return { op: 'pageload' }; + }, +}; + +jest.mock('@sentry/browser', () => { + const actual = jest.requireActual('@sentry/browser'); + return { + ...actual, + startBrowserTracingNavigationSpan: (...args: unknown[]) => { + mockStartBrowserTracingNavigationSpan(...args); + return actual.startBrowserTracingNavigationSpan(...args); + }, + startBrowserTracingPageLoadSpan: (...args: unknown[]) => { + mockStartBrowserTracingPageLoadSpan(...args); + return actual.startBrowserTracingPageLoadSpan(...args); + }, + }; +}); + +jest.mock('@sentry/core', () => { + const actual = jest.requireActual('@sentry/core'); + return { + ...actual, + getRootSpan: () => { + return mockRootSpan; + }, + }; +}); + +describe('browserTracingReactRouterV4', () => { + function createMockBrowserClient(): BrowserClient { + return new BrowserClient({ + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + debug: true, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + getCurrentScope().setClient(undefined); + }); + + it('starts a pageload transaction when instrumentation is started', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }, + }); + }); + + it('starts a navigation transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + render( + + +

Features
} /> +
About
} /> +
Home
} /> + + , + ); + + act(() => { + history.push('/about'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + + act(() => { + history.push('/features'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/features', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); + + it('only starts a navigation transaction on push', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + render( + + +
Features
} /> +
About
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.replace('hello'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(0); + }); + + it('does not normalize transaction name ', () => { + const client = createMockBrowserClient(); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + const { getByText } = render( + + +
UserId
} /> +
Users
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/users/123'); + }); + getByText('UserId'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/users/123', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); + + it('normalizes transaction name with custom Route', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + const SentryRoute = withSentryRouting(Route); + + const { getByText } = render( + + +
UserId
} /> +
Users
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/users/123'); + }); + getByText('UserId'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/users/123', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/users/:userid'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('normalizes nested transaction names with custom Route', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + const SentryRoute = withSentryRouting(Route); + + const { getByText } = render( + + +
Team
} /> +
OrgId
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/organizations/1234/v1/758'); + }); + getByText('Team'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/1234/v1/758', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); + expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + act(() => { + history.push('/organizations/543'); + }); + getByText('OrgId'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/543', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + expect(mockRootSpan.updateName).toHaveBeenCalledTimes(3); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/organizations/:orgid'); + expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('matches with route object', () => { + const routes: RouteConfig[] = [ + { + path: '/organizations/:orgid/v1/:teamid', + }, + { path: '/organizations/:orgid' }, + { path: '/' }, + ]; + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history, routes, matchPath })); + + client.init(); + + render( + + +
Team
} /> +
OrgId
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/organizations/1234/v1/758'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/:orgid/v1/:teamid', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + + act(() => { + history.push('/organizations/1234'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/:orgid', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); }); }); diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index c571b3590b8f..b08f7de702a1 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,14 +1,26 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + createTransport, + getCurrentScope, + setCurrentClient, +} from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Route, Router, Switch, matchPath } from 'react-router-5'; -import { reactRouterV5Instrumentation, withSentryRouting } from '../src'; +import { + BrowserClient, + reactRouterV5BrowserTracingIntegration, + reactRouterV5Instrumentation, + withSentryRouting, +} from '../src'; import type { RouteConfig } from '../src/reactrouter'; -describe('React Router v5', () => { +describe('reactRouterV5Instrumentation', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; @@ -28,6 +40,7 @@ describe('React Router v5', () => { const mockStartTransaction = jest .fn() .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); + // eslint-disable-next-line deprecation/deprecation reactRouterV5Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, @@ -41,10 +54,11 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }, }); }); @@ -71,10 +85,11 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); act(() => { @@ -83,10 +98,11 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/features', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); }); @@ -162,17 +178,17 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/users/123', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); }); it('normalizes transaction name with custom Route', () => { const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); - const { getByText } = render( @@ -182,6 +198,7 @@ describe('React Router v5', () => { , ); + act(() => { history.push('/users/123'); }); @@ -190,20 +207,20 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/users/123', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); - const { getByText } = render( @@ -222,10 +239,11 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/1234/v1/758', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); @@ -239,13 +257,15 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/543', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); expect(mockUpdateName).toHaveBeenCalledTimes(3); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid'); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('matches with route object', () => { @@ -273,10 +293,11 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid/v1/:teamid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); act(() => { @@ -285,10 +306,339 @@ describe('React Router v5', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - tags: { 'routing.instrumentation': 'react-router-v5' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); +}); + +const mockStartBrowserTracingPageLoadSpan = jest.fn(); +const mockStartBrowserTracingNavigationSpan = jest.fn(); + +const mockRootSpan = { + updateName: jest.fn(), + setAttribute: jest.fn(), + getSpanJSON() { + return { op: 'pageload' }; + }, +}; + +jest.mock('@sentry/browser', () => { + const actual = jest.requireActual('@sentry/browser'); + return { + ...actual, + startBrowserTracingNavigationSpan: (...args: unknown[]) => { + mockStartBrowserTracingNavigationSpan(...args); + return actual.startBrowserTracingNavigationSpan(...args); + }, + startBrowserTracingPageLoadSpan: (...args: unknown[]) => { + mockStartBrowserTracingPageLoadSpan(...args); + return actual.startBrowserTracingPageLoadSpan(...args); + }, + }; +}); + +jest.mock('@sentry/core', () => { + const actual = jest.requireActual('@sentry/core'); + return { + ...actual, + getRootSpan: () => { + return mockRootSpan; + }, + }; +}); + +describe('browserTracingReactRouterV5', () => { + function createMockBrowserClient(): BrowserClient { + return new BrowserClient({ + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + debug: true, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + getCurrentScope().setClient(undefined); + }); + + it('starts a pageload transaction when instrumentation is started', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }, + }); + }); + + it('starts a navigation transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + render( + + +
Features
} /> +
About
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/about'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + + act(() => { + history.push('/features'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/features', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); + + it('only starts a navigation transaction on push', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + render( + + +
Features
} /> +
About
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.replace('hello'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(0); + }); + + it('does not normalize transaction name ', () => { + const client = createMockBrowserClient(); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + const { getByText } = render( + + +
UserId
} /> +
Users
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/users/123'); + }); + getByText('UserId'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/users/123', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); + + it('normalizes transaction name with custom Route', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + const SentryRoute = withSentryRouting(Route); + + const { getByText } = render( + + +
UserId
} /> +
Users
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/users/123'); + }); + getByText('UserId'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/users/123', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/users/:userid'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('normalizes nested transaction names with custom Route', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + const SentryRoute = withSentryRouting(Route); + + const { getByText } = render( + + +
Team
} /> +
OrgId
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/organizations/1234/v1/758'); + }); + getByText('Team'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/1234/v1/758', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); + expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + act(() => { + history.push('/organizations/543'); + }); + getByText('OrgId'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/543', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + expect(mockRootSpan.updateName).toHaveBeenCalledTimes(3); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/organizations/:orgid'); + expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('matches with route object', () => { + const routes: RouteConfig[] = [ + { + path: '/organizations/:orgid/v1/:teamid', + }, + { path: '/organizations/:orgid' }, + { path: '/' }, + ]; + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history, routes, matchPath })); + + client.init(); + + render( + + +
Team
} /> +
OrgId
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/organizations/1234/v1/758'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/:orgid/v1/:teamid', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + + act(() => { + history.push('/organizations/1234'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/organizations/:orgid', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, }); }); }); From e4806d1d08e564281e20ac3e61fecbb5463d9b33 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 5 Feb 2024 17:39:00 +0100 Subject: [PATCH 53/68] feat(react): Add `reactRouterV6BrowserTracingIntegration` for react router v6 & v6.4 (#10491) feat(react): Add browserTracingIntegrations for router v4 & v5 This adds a new `reactRouterV6BrowserTracingIntegration()` exports deprecating the old routing instrumentation. I opted to leave as much as possible as-is for now, except for streamlining the attributes/tags we use for the instrumentation. I also updated the E2E tests to the new format. --- .../react-create-hash-router/src/index.tsx | 14 +- .../react-router-6-use-routes/src/index.tsx | 14 +- .../standard-frontend-react/src/index.tsx | 14 +- packages/react/src/index.ts | 2 + packages/react/src/reactrouterv6.tsx | 160 ++- packages/react/test/reactrouterv6.4.test.tsx | 662 +++++++++++- packages/react/test/reactrouterv6.test.tsx | 996 ++++++++++++++++-- 7 files changed, 1707 insertions(+), 155 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx index 35db65cf3160..73c5e024539f 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx @@ -18,14 +18,12 @@ Sentry.init({ // environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - new Sentry.BrowserTracing({ - routingInstrumentation: Sentry.reactRouterV6Instrumentation( - React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - ), + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, }), replay, ], diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx index 2f8587db9859..b8a036fc5340 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx @@ -18,14 +18,12 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - new Sentry.BrowserTracing({ - routingInstrumentation: Sentry.reactRouterV6Instrumentation( - React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - ), + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, }), replay, ], diff --git a/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx b/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx index 660c3827f583..8cf0e8462e16 100644 --- a/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx @@ -19,14 +19,12 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ - new Sentry.BrowserTracing({ - routingInstrumentation: Sentry.reactRouterV6Instrumentation( - React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - ), + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, }), replay, ], diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index c8be42b00d3b..d1c22d6966bb 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -16,7 +16,9 @@ export { reactRouterV5BrowserTracingIntegration, } from './reactrouter'; export { + // eslint-disable-next-line deprecation/deprecation reactRouterV6Instrumentation, + reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, wrapUseRoutes, wrapCreateBrowserRouter, diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index c2dc56687571..73196bcfcc2a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -1,9 +1,29 @@ +/* eslint-disable max-lines */ // Inspired from Donnie McNeal's solution: // https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 -import { WINDOW } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import { + WINDOW, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, +} from '@sentry/browser'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + getActiveSpan, + getRootSpan, + spanToJSON, +} from '@sentry/core'; +import type { + Integration, + Span, + StartSpanOptions, + Transaction, + TransactionContext, + TransactionSource, +} from '@sentry/types'; import { getNumberOfUrlSegments, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -37,10 +57,77 @@ let _customStartTransaction: (context: TransactionContext) => Transaction | unde let _startTransactionOnLocationChange: boolean; let _stripBasename: boolean = false; -const SENTRY_TAGS = { - 'routing.instrumentation': 'react-router-v6', -}; +interface ReactRouterOptions { + useEffect: UseEffect; + useLocation: UseLocation; + useNavigationType: UseNavigationType; + createRoutesFromChildren: CreateRoutesFromChildren; + matchRoutes: MatchRoutes; + stripBasename?: boolean; +} + +/** + * A browser tracing integration that uses React Router v3 to instrument navigations. + * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options. + */ +export function reactRouterV6BrowserTracingIntegration( + options: Parameters[0] & ReactRouterOptions, +): Integration { + const integration = browserTracingIntegration({ + ...options, + instrumentPageLoad: false, + instrumentNavigation: false, + }); + + const { + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + stripBasename, + instrumentPageLoad = true, + instrumentNavigation = true, + } = options; + + return { + ...integration, + afterAllSetup(client) { + integration.afterAllSetup(client); + + const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingNavigationSpan(client, startSpanOptions); + return undefined; + }; + + const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname; + if (instrumentPageLoad && initPathName) { + startBrowserTracingPageLoadSpan(client, { + name: initPathName, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, + }); + } + _useEffect = useEffect; + _useLocation = useLocation; + _useNavigationType = useNavigationType; + _matchRoutes = matchRoutes; + _createRoutesFromChildren = createRoutesFromChildren; + _stripBasename = stripBasename || false; + + _customStartTransaction = startNavigationCallback; + _startTransactionOnLocationChange = instrumentNavigation; + }, + }; +} + +/** + * @deprecated Use `reactRouterV6BrowserTracingIntegration()` instead. + */ export function reactRouterV6Instrumentation( useEffect: UseEffect, useLocation: UseLocation, @@ -58,11 +145,10 @@ export function reactRouterV6Instrumentation( if (startTransactionOnPageLoad && initPathName) { activeTransaction = customStartTransaction({ name: initPathName, - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv6', - tags: SENTRY_TAGS, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); } @@ -155,6 +241,7 @@ function getNormalizedName( } function updatePageloadTransaction( + activeRootSpan: Span | undefined, location: Location, routes: RouteObject[], matches?: AgnosticDataRouteMatch, @@ -164,10 +251,10 @@ function updatePageloadTransaction( ? matches : (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]); - if (activeTransaction && branches) { + if (activeRootSpan && branches) { const [name, source] = getNormalizedName(routes, location, branches, basename); - activeTransaction.updateName(name); - activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + activeRootSpan.updateName(name); + activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } @@ -188,11 +275,10 @@ function handleNavigation( const [name, source] = getNormalizedName(routes, location, branches, basename); activeTransaction = _customStartTransaction({ name, - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: SENTRY_TAGS, - metadata: { - source, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); } @@ -227,7 +313,7 @@ export function withSentryReactRouterV6Routing

, R const routes = _createRoutesFromChildren(props.children) as RouteObject[]; if (isMountRenderPass) { - updatePageloadTransaction(location, routes); + updatePageloadTransaction(getActiveRootSpan(), location, routes); isMountRenderPass = false; } else { handleNavigation(location, routes, navigationType); @@ -285,7 +371,7 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; if (isMountRenderPass) { - updatePageloadTransaction(normalizedLocation, routes); + updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes); isMountRenderPass = false; } else { handleNavigation(normalizedLocation, routes, navigationType); @@ -312,21 +398,18 @@ export function wrapCreateBrowserRouter< const router = createRouterFunction(routes, opts); const basename = opts && opts.basename; + const activeRootSpan = getActiveRootSpan(); + // The initial load ends when `createBrowserRouter` is called. // This is the earliest convenient time to update the transaction name. // Callbacks to `router.subscribe` are not called for the initial load. - if (router.state.historyAction === 'POP' && activeTransaction) { - updatePageloadTransaction(router.state.location, routes, undefined, basename); + if (router.state.historyAction === 'POP' && activeRootSpan) { + updatePageloadTransaction(activeRootSpan, router.state.location, routes, undefined, basename); } router.subscribe((state: RouterState) => { const location = state.location; - - if ( - _startTransactionOnLocationChange && - (state.historyAction === 'PUSH' || state.historyAction === 'POP') && - activeTransaction - ) { + if (_startTransactionOnLocationChange && (state.historyAction === 'PUSH' || state.historyAction === 'POP')) { handleNavigation(location, routes, state.historyAction, undefined, basename); } }); @@ -334,3 +417,22 @@ export function wrapCreateBrowserRouter< return router; }; } + +function getActiveRootSpan(): Span | undefined { + // Legacy behavior for "old" react router instrumentation + if (activeTransaction) { + return activeTransaction; + } + + const span = getActiveSpan(); + const rootSpan = span ? getRootSpan(span) : undefined; + + if (!rootSpan) { + return undefined; + } + + const op = spanToJSON(rootSpan).op; + + // Only use this root span if it is a pageload or navigation span + return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; +} diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index 29fe612f7e97..f534d02f97e2 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -1,4 +1,11 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + createTransport, + getCurrentScope, + setCurrentClient, +} from '@sentry/core'; import { render } from '@testing-library/react'; import { Request } from 'node-fetch'; import * as React from 'react'; @@ -13,7 +20,8 @@ import { useNavigationType, } from 'react-router-6.4'; -import { reactRouterV6Instrumentation, wrapCreateBrowserRouter } from '../src'; +import { BrowserClient, reactRouterV6Instrumentation, wrapCreateBrowserRouter } from '../src'; +import { reactRouterV6BrowserTracingIntegration } from '../src/reactrouterv6'; import type { CreateRouterFunction } from '../src/types'; beforeAll(() => { @@ -22,7 +30,7 @@ beforeAll(() => { global.Request = Request; }); -describe('React Router v6.4', () => { +describe('reactRouterV6Instrumentation (v6.4)', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; @@ -41,6 +49,7 @@ describe('React Router v6.4', () => { .fn() .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); + // eslint-disable-next-line deprecation/deprecation reactRouterV6Instrumentation( React.useEffect, useLocation, @@ -75,13 +84,10 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv6', - tags: { - 'routing.instrumentation': 'react-router-v6', - }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); }); @@ -112,10 +118,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -151,10 +158,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/us', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -190,10 +198,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/:page', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -241,10 +250,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/stores/:storeId/products/:productId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -311,10 +321,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/app/about/us', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -355,10 +366,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/admin/:orgId/users/:userId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -401,10 +413,11 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/:orgId/users/:userId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -443,10 +456,575 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/us', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + }); +}); + +const mockStartBrowserTracingPageLoadSpan = jest.fn(); +const mockStartBrowserTracingNavigationSpan = jest.fn(); + +const mockRootSpan = { + updateName: jest.fn(), + setAttribute: jest.fn(), + getSpanJSON() { + return { op: 'pageload' }; + }, +}; + +jest.mock('@sentry/browser', () => { + const actual = jest.requireActual('@sentry/browser'); + return { + ...actual, + startBrowserTracingNavigationSpan: (...args: unknown[]) => { + mockStartBrowserTracingNavigationSpan(...args); + return actual.startBrowserTracingNavigationSpan(...args); + }, + startBrowserTracingPageLoadSpan: (...args: unknown[]) => { + mockStartBrowserTracingPageLoadSpan(...args); + return actual.startBrowserTracingPageLoadSpan(...args); + }, + }; +}); + +jest.mock('@sentry/core', () => { + const actual = jest.requireActual('@sentry/core'); + return { + ...actual, + getRootSpan: () => { + return mockRootSpan; + }, + }; +}); + +describe('reactRouterV6BrowserTracingIntegration (v6.4)', () => { + function createMockBrowserClient(): BrowserClient { + return new BrowserClient({ + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + debug: true, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + getCurrentScope().setClient(undefined); + }); + + describe('wrapCreateBrowserRouter', () => { + it('starts a pageload transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element:

TEST
, + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, + }); + }); + + it('starts a navigation transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with nested routes', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + children: [ + { + path: 'us', + element:
Us
, + }, + ], + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/us', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with parameterized paths', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + children: [ + { + path: ':page', + element:
Page
, + }, + ], + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/:page', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with paths with multiple parameters', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'stores', + element:
Stores
, + children: [ + { + path: ':storeId', + element:
Store
, + children: [ + { + path: 'products', + element:
Products
, + children: [ + { + path: ':productId', + element:
Product
, + }, + ], + }, + ], + }, + ], + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/stores/:storeId/products/:productId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('updates pageload transaction to a parameterized route', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: 'about', + element:
About
, + children: [ + { + path: ':page', + element:
page
, + }, + ], + }, + ], + { + initialEntries: ['/about/us'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about/:page'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('works with `basename` option', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + children: [ + { + path: 'us', + element:
Us
, + }, + ], + }, + ], + { + initialEntries: ['/app'], + basename: '/app', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/app/about/us', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with parameterized paths and `basename`', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: ':orgId', + children: [ + { + path: 'users', + children: [ + { + path: ':userId', + element:
User
, + }, + ], + }, + ], + }, + ], + { + initialEntries: ['/admin'], + basename: '/admin', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/admin/:orgId/users/:userId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('strips `basename` from transaction names of parameterized paths', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + stripBasename: true, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: ':orgId', + children: [ + { + path: 'users', + children: [ + { + path: ':userId', + element:
User
, + }, + ], + }, + ], + }, + ], + { + initialEntries: ['/admin'], + basename: '/admin', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/:orgId/users/:userId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('strips `basename` from transaction names of non-parameterized paths', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + stripBasename: true, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + children: [ + { + path: 'us', + element:
Us
, + }, + ], + }, + ], + { + initialEntries: ['/app'], + basename: '/app', + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/us', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); }); diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index df30c4596dbf..f2ec3fb3a4b9 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,4 +1,11 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + createTransport, + getCurrentScope, + setCurrentClient, +} from '@sentry/core'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -15,10 +22,14 @@ import { useRoutes, } from 'react-router-6'; -import { reactRouterV6Instrumentation } from '../src'; -import { withSentryReactRouterV6Routing, wrapUseRoutes } from '../src/reactrouterv6'; +import { BrowserClient, reactRouterV6Instrumentation } from '../src'; +import { + reactRouterV6BrowserTracingIntegration, + withSentryReactRouterV6Routing, + wrapUseRoutes, +} from '../src/reactrouterv6'; -describe('React Router v6', () => { +describe('reactRouterV6Instrumentation', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; @@ -36,6 +47,7 @@ describe('React Router v6', () => { .fn() .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); + // eslint-disable-next-line deprecation/deprecation reactRouterV6Instrumentation( React.useEffect, useLocation, @@ -62,10 +74,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, }); }); @@ -100,10 +113,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, }); }); @@ -123,10 +137,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -148,10 +163,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/us', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -173,10 +189,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/:page', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -200,10 +217,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/stores/:storeId/products/:productId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -235,10 +253,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/projects/:projectId/views/:viewId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); }); @@ -265,10 +284,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, }); }); @@ -318,10 +338,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, }); }); @@ -350,10 +371,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -388,10 +410,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/us', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -426,10 +449,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about/:page', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -470,10 +494,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/stores/:storeId/products/:productId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -538,10 +563,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/projects/:projectId/views/:viewId', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv6', - tags: { 'routing.instrumentation': 'react-router-v6' }, - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, }); }); @@ -639,3 +665,853 @@ describe('React Router v6', () => { }); }); }); + +const mockStartBrowserTracingPageLoadSpan = jest.fn(); +const mockStartBrowserTracingNavigationSpan = jest.fn(); + +const mockRootSpan = { + updateName: jest.fn(), + setAttribute: jest.fn(), + getSpanJSON() { + return { op: 'pageload' }; + }, +}; + +jest.mock('@sentry/browser', () => { + const actual = jest.requireActual('@sentry/browser'); + return { + ...actual, + startBrowserTracingNavigationSpan: (...args: unknown[]) => { + mockStartBrowserTracingNavigationSpan(...args); + return actual.startBrowserTracingNavigationSpan(...args); + }, + startBrowserTracingPageLoadSpan: (...args: unknown[]) => { + mockStartBrowserTracingPageLoadSpan(...args); + return actual.startBrowserTracingPageLoadSpan(...args); + }, + }; +}); + +jest.mock('@sentry/core', () => { + const actual = jest.requireActual('@sentry/core'); + return { + ...actual, + getRootSpan: () => { + return mockRootSpan; + }, + }; +}); + +describe('reactRouterV6BrowserTracingIntegration', () => { + function createMockBrowserClient(): BrowserClient { + return new BrowserClient({ + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + debug: true, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + getCurrentScope().setClient(undefined); + }); + + describe('withSentryReactRouterV6Routing', () => { + it('starts a pageload transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Home} /> + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, + }); + }); + + it('skips pageload transaction with `instrumentPageLoad: false`', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + instrumentPageLoad: false, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Home} /> + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(0); + }); + + it('skips navigation transaction, with `instrumentNavigation: false`', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + instrumentNavigation: false, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About} /> + } /> + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(0); + }); + + it('starts a navigation transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About} /> + } /> + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with nested routes', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + us} /> + + } /> + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/us', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with paramaterized paths', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + page} /> + + } /> + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/:page', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with paths with multiple parameters', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Stores}> + Store}> + Product} /> + + + } /> + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/stores/:storeId/products/:productId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with nested paths with parameters', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + } /> + Account Page} /> + + Project Index} /> + Project Page}> + Project Page Root} /> + Editor}> + View Canvas} /> + Space Canvas} /> + + + + + No Match Page} /> + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/projects/:projectId/views/:viewId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + }); + + describe('wrapUseRoutes', () => { + it('starts a pageload transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element:
Home
, + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + }, + }); + }); + + it('skips pageload transaction with `instrumentPageLoad: false`', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + instrumentPageLoad: false, + }), + ); + + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element:
Home
, + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(0); + }); + + it('skips navigation transaction, with `instrumentNavigation: false`', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + instrumentNavigation: false, + }), + ); + + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(0); + }); + + it('starts a navigation transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with nested routes', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + children: [ + { + path: '/about/us', + element:
us
, + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/us', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with paramaterized paths', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + children: [ + { + path: '/about/:page', + element:
page
, + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about/:page', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with paths with multiple parameters', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/stores', + element:
Stores
, + children: [ + { + path: '/stores/:storeId', + element:
Store
, + children: [ + { + path: '/stores/:storeId/products/:productId', + element:
Product
, + }, + ], + }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/stores/:storeId/products/:productId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('works with nested paths with parameters', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + index: true, + element: , + }, + { + path: 'account', + element:
Account Page
, + }, + { + path: 'projects', + children: [ + { + index: true, + element:
Project Index
, + }, + { + path: ':projectId', + element:
Project Page
, + children: [ + { + index: true, + element:
Project Page Root
, + }, + { + element:
Editor
, + children: [ + { + path: 'views/:viewId', + element:
View Canvas
, + }, + { + path: 'spaces/:spaceId', + element:
Space Canvas
, + }, + ], + }, + ], + }, + ], + }, + { + path: '*', + element:
No Match Page
, + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/projects/:projectId/views/:viewId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + + it('does not add double slashes to URLS', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: ( +
+ +
+ ), + children: [ + { + path: 'tests', + children: [ + { index: true, element:
Main Test
}, + { path: ':testId/*', element:
Test Component
}, + ], + }, + { path: '/', element: }, + { path: '*', element: }, + ], + }, + { + path: '/', + element:
, + children: [ + { path: '404', element:
Error
}, + { path: '*', element: }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + // should be /tests not //tests + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/tests'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('handles wildcard routes properly', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: ( +
+ +
+ ), + children: [ + { + path: 'tests', + children: [ + { index: true, element:
Main Test
}, + { path: ':testId/*', element:
Test Component
}, + ], + }, + { path: '/', element: }, + { path: '*', element: }, + ], + }, + { + path: '/', + element:
, + children: [ + { path: '404', element:
Error
}, + { path: '*', element: }, + ], + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/tests/:testId/*'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + }); +}); From 2227b277aa85659a6d13d3c6934c3d4fb35342ec Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 5 Feb 2024 17:54:47 +0100 Subject: [PATCH 54/68] feat(react): Add `reactRouterV3BrowserTracingIntegration` for react router v3 (#10489) To replace the routing instrumentation. There is a _small_ issue here, which is that we do not set the `from` attribute for the first navigation after the pageload (as technically we are calling instrument twice there...) - IMHO that's acceptable, we don't really have a `from` field anyhow in other instrumentations, so we may even think about removing this I'd say... --- packages/react/src/index.ts | 6 +- packages/react/src/reactrouterv3.ts | 100 ++++++-- packages/react/test/reactrouterv3.test.tsx | 255 +++++++++++++++++---- 3 files changed, 298 insertions(+), 63 deletions(-) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d1c22d6966bb..2fa3e32e67d6 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -5,7 +5,11 @@ export { Profiler, withProfiler, useProfiler } from './profiler'; export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; export { ErrorBoundary, withErrorBoundary } from './errorboundary'; export { createReduxEnhancer } from './redux'; -export { reactRouterV3Instrumentation } from './reactrouterv3'; +export { + // eslint-disable-next-line deprecation/deprecation + reactRouterV3Instrumentation, + reactRouterV3BrowserTracingIntegration, +} from './reactrouterv3'; export { // eslint-disable-next-line deprecation/deprecation reactRouterV4Instrumentation, diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index db1ce1320508..905ebec13897 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -1,5 +1,22 @@ -import { WINDOW } from '@sentry/browser'; -import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import { + WINDOW, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, +} from '@sentry/browser'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; +import type { + Integration, + SpanAttributes, + StartSpanOptions, + Transaction, + TransactionContext, + TransactionSource, +} from '@sentry/types'; import type { Location, ReactRouterInstrumentation } from './types'; @@ -21,6 +38,52 @@ export type Match = ( type ReactRouterV3TransactionSource = Extract; +interface ReactRouterOptions { + history: HistoryV3; + routes: Route[]; + match: Match; +} + +/** + * A browser tracing integration that uses React Router v3 to instrument navigations. + * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options. + */ +export function reactRouterV3BrowserTracingIntegration( + options: Parameters[0] & ReactRouterOptions, +): Integration { + const integration = browserTracingIntegration({ + ...options, + instrumentPageLoad: false, + instrumentNavigation: false, + }); + + const { history, routes, match, instrumentPageLoad = true, instrumentNavigation = true } = options; + + return { + ...integration, + afterAllSetup(client) { + integration.afterAllSetup(client); + + const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingPageLoadSpan(client, startSpanOptions); + return undefined; + }; + + const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => { + startBrowserTracingNavigationSpan(client, startSpanOptions); + return undefined; + }; + + // eslint-disable-next-line deprecation/deprecation + const instrumentation = reactRouterV3Instrumentation(history, routes, match); + + // Now instrument page load & navigation with correct settings + instrumentation(startPageloadCallback, instrumentPageLoad, false); + instrumentation(startNavigationCallback, false, instrumentNavigation); + }, + }; +} + /** * Creates routing instrumentation for React Router v3 * Works for React Router >= 3.2.0 and < 4.0.0 @@ -28,6 +91,8 @@ type ReactRouterV3TransactionSource = Extract = { - 'routing.instrumentation': 'react-router-v3', - }; - if (prevName) { - tags.from = prevName; - } normalizeTransactionName(routes, location, match, (localName: string, source: TransactionSource = 'url') => { prevName = localName; + + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + }; + activeTransaction = startTransaction({ name: prevName, - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags, - metadata: { - source, - }, + attributes, }); }); } diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index 21b9054e45ce..c9926567cea4 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -1,8 +1,18 @@ +import { BrowserClient } from '@sentry/browser'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + createTransport, + getCurrentScope, + setCurrentClient, +} from '@sentry/core'; import { act, render } from '@testing-library/react'; import * as React from 'react'; import { IndexRoute, Route, Router, createMemoryHistory, createRoutes, match } from 'react-router-3'; import type { Match, Route as RouteType } from '../src/reactrouterv3'; +import { reactRouterV3BrowserTracingIntegration } from '../src/reactrouterv3'; import { reactRouterV3Instrumentation } from '../src/reactrouterv3'; // Have to manually set types because we are using package-alias @@ -24,7 +34,7 @@ function createMockStartTransaction(opts: { finish?: jest.FunctionLike; setMetad }); } -describe('React Router V3', () => { +describe('reactRouterV3Instrumentation', () => { const routes = (
{children}
}>
Home
} /> @@ -43,6 +53,7 @@ describe('React Router V3', () => { const history = createMemoryHistory(); const instrumentationRoutes = createRoutes(routes); + // eslint-disable-next-line deprecation/deprecation const instrumentation = reactRouterV3Instrumentation(history, instrumentationRoutes, match); it('starts a pageload transaction when instrumentation is started', () => { @@ -51,11 +62,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv3', - tags: { 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'route', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', }, }); }); @@ -77,11 +87,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/about', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags: { from: '/', 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'route', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); @@ -91,11 +100,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/features', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags: { from: '/about', 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'route', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); }); @@ -145,11 +153,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/users/:userid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags: { from: '/', 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'route', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); }); @@ -167,11 +174,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid/v1/:teamid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags: { from: '/', 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'route', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); @@ -183,11 +189,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(3); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/:orgid', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags: { from: '/organizations/:orgid/v1/:teamid', 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'route', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); }); @@ -204,11 +209,10 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/organizations/1234/some/other/route', - op: 'navigation', - origin: 'auto.navigation.react.reactrouterv3', - tags: { from: '/', 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); }); @@ -216,6 +220,7 @@ describe('React Router V3', () => { it('sets metadata to url if no routes are provided', () => { const fakeRoutes =
hello
; const mockStartTransaction = createMockStartTransaction(); + // eslint-disable-next-line deprecation/deprecation const mockInstrumentation = reactRouterV3Instrumentation(history, createRoutes(fakeRoutes), match); mockInstrumentation(mockStartTransaction); // We render here with `routes` instead of `fakeRoutes` from above to validate the case @@ -225,11 +230,179 @@ describe('React Router V3', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/', - op: 'pageload', - origin: 'auto.pageload.react.reactrouterv3', - tags: { 'routing.instrumentation': 'react-router-v3' }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }, + }); + }); +}); + +const mockStartBrowserTracingPageLoadSpan = jest.fn(); +const mockStartBrowserTracingNavigationSpan = jest.fn(); + +const mockRootSpan = { + updateName: jest.fn(), + setAttribute: jest.fn(), + getSpanJSON() { + return { op: 'pageload' }; + }, +}; + +jest.mock('@sentry/browser', () => { + const actual = jest.requireActual('@sentry/browser'); + return { + ...actual, + startBrowserTracingNavigationSpan: (...args: unknown[]) => { + mockStartBrowserTracingNavigationSpan(...args); + return actual.startBrowserTracingNavigationSpan(...args); + }, + startBrowserTracingPageLoadSpan: (...args: unknown[]) => { + mockStartBrowserTracingPageLoadSpan(...args); + return actual.startBrowserTracingPageLoadSpan(...args); + }, + }; +}); + +jest.mock('@sentry/core', () => { + const actual = jest.requireActual('@sentry/core'); + return { + ...actual, + getRootSpan: () => { + return mockRootSpan; + }, + }; +}); + +describe('browserTracingReactRouterV3', () => { + const routes = ( +
{children}
}> +
Home
} /> +
About
} /> +
Features
} /> + }) =>
{params.userid}
} + /> + +
OrgId
} /> +
Team
} /> +
+
+ ); + const history = createMemoryHistory(); + + const instrumentationRoutes = createRoutes(routes); + + function createMockBrowserClient(): BrowserClient { + return new BrowserClient({ + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + debug: true, + }); + } + + beforeEach(() => { + jest.clearAllMocks(); + getCurrentScope().setClient(undefined); + }); + + it('starts a pageload transaction when instrumentation is started', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration(reactRouterV3BrowserTracingIntegration({ history, routes: instrumentationRoutes, match })); + + client.init(); + render({routes}); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }, + }); + }); + + it('starts a navigation transaction', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV3BrowserTracingIntegration({ history, routes: instrumentationRoutes, match })); + + client.init(); + render({routes}); + + act(() => { + history.push('/about'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/about', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + + act(() => { + history.push('/features'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(2); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/features', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + }, + }); + }); + + it('only starts a navigation transaction on push', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV3BrowserTracingIntegration({ history, routes: instrumentationRoutes, match })); + + client.init(); + render({routes}); + + act(() => { + history.replace('hello'); + }); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(0); + }); + + it('normalizes transaction name ', () => { + const client = createMockBrowserClient(); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV3BrowserTracingIntegration({ history, routes: instrumentationRoutes, match })); + + client.init(); + const { container } = render({routes}); + + act(() => { + history.push('/users/123'); + }); + expect(container.innerHTML).toContain('123'); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/users/:userid', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', }, }); }); From 60a7d65605d7db4de854e4071896abf168e6bdc5 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 5 Feb 2024 14:42:26 -0500 Subject: [PATCH 55/68] Revert "feat(core): Add metric summaries to spans (#10432)" (#10495) This reverts commit 94cdd8bda146455b7b4a46ed52a233b9394bd953. I mistakenly merged this - we have to fix the metric summaries format. Summaries are typed as `pub type MetricSummaryMapping = Object>;` Which means that the `_metrics_summary` type needs to be `_metrics_summary?: Record>;`. This is because we need to create separate entries if the tags have changed. ``` "c:processor.item_processed": [ { "min": 1, "max": 1, "count": 3, "sum": 3, "tags": {"success": true} }, { "min": 1, "max": 1, "count": 2, "sum": 2, "tags": {"success": false} } ], ``` --- .../tracing/metric-summaries/scenario.js | 48 ---------- .../suites/tracing/metric-summaries/test.ts | 69 --------------- packages/core/src/metrics/aggregator.ts | 11 +-- .../core/src/metrics/browser-aggregator.ts | 27 +++--- packages/core/src/metrics/metric-summary.ts | 87 ------------------- packages/core/src/tracing/span.ts | 2 - packages/core/src/tracing/transaction.ts | 2 - packages/types/src/event.ts | 3 +- packages/types/src/index.ts | 7 +- packages/types/src/span.ts | 9 -- 10 files changed, 15 insertions(+), 250 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js delete mode 100644 dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts delete mode 100644 packages/core/src/metrics/metric-summary.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 deleted file mode 100644 index 8a7dbabe0dec..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js +++ /dev/null @@ -1,48 +0,0 @@ -const { loggingTransport } = require('@sentry-internal/node-integration-tests'); -const Sentry = require('@sentry/node'); - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, - _experiments: { - metricsAggregator: true, - }, -}); - -// Stop the process from exiting before the transaction is sent -setInterval(() => {}, 1000); - -Sentry.startSpan( - { - name: 'Test Transaction', - op: 'transaction', - }, - () => { - Sentry.metrics.increment('root-counter'); - Sentry.metrics.increment('root-counter'); - - Sentry.startSpan( - { - name: 'Some other span', - op: 'transaction', - }, - () => { - Sentry.metrics.increment('root-counter'); - Sentry.metrics.increment('root-counter'); - Sentry.metrics.increment('root-counter', 2); - - Sentry.metrics.set('root-set', 'some-value'); - Sentry.metrics.set('root-set', 'another-value'); - Sentry.metrics.set('root-set', 'another-value'); - - Sentry.metrics.gauge('root-gauge', 42); - Sentry.metrics.gauge('root-gauge', 20); - - Sentry.metrics.distribution('root-distribution', 42); - Sentry.metrics.distribution('root-distribution', 20); - }, - ); - }, -); diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts deleted file mode 100644 index 98ed58a75c57..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { createRunner } from '../../../utils/runner'; - -const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - _metrics_summary: { - 'c:root-counter@none': { - min: 1, - max: 1, - count: 2, - sum: 2, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - }, - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'Some other span', - op: 'transaction', - _metrics_summary: { - 'c:root-counter@none': { - min: 1, - max: 2, - count: 3, - sum: 4, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - 's:root-set@none': { - min: 0, - max: 1, - count: 3, - sum: 2, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - 'g:root-gauge@none': { - min: 20, - max: 42, - count: 2, - sum: 62, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - 'd:root-distribution@none': { - min: 20, - max: 42, - count: 2, - sum: 62, - tags: { - release: '1.0', - transaction: 'Test Transaction', - }, - }, - }, - }), - ]), -}; - -test('Should add metric summaries to spans', done => { - createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); -}); diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index 2b331082ab3e..6a49fda5918b 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -6,9 +6,8 @@ import type { Primitive, } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; +import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; import { METRIC_MAP } from './instance'; -import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -63,11 +62,7 @@ export class MetricsAggregator implements MetricsAggregatorBase { const tags = sanitizeTags(unsanitizedTags); const bucketKey = getBucketKey(metricType, name, unit, tags); - let bucketItem = this._buckets.get(bucketKey); - // If this is a set metric, we need to calculate the delta from the previous weight. - const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; - if (bucketItem) { bucketItem.metric.add(value); // TODO(abhi): Do we need this check? @@ -87,10 +82,6 @@ export class MetricsAggregator implements MetricsAggregatorBase { this._buckets.set(bucketKey, bucketItem); } - // If value is a string, it's a set metric so calculate the delta from the previous weight. - const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; - updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); - // We need to keep track of the total weight of the buckets so that we can // flush them when we exceed the max weight. this._bucketsTotalWeight += bucketItem.metric.weight; diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts index 40cfa1d404ab..5b5c81353024 100644 --- a/packages/core/src/metrics/browser-aggregator.ts +++ b/packages/core/src/metrics/browser-aggregator.ts @@ -1,8 +1,14 @@ -import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; +import type { + Client, + ClientOptions, + MeasurementUnit, + MetricBucketItem, + MetricsAggregator, + Primitive, +} from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; +import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; import { METRIC_MAP } from './instance'; -import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -40,11 +46,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator { const tags = sanitizeTags(unsanitizedTags); const bucketKey = getBucketKey(metricType, name, unit, tags); - - let bucketItem = this._buckets.get(bucketKey); - // If this is a set metric, we need to calculate the delta from the previous weight. - const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; - + const bucketItem: MetricBucketItem | undefined = this._buckets.get(bucketKey); if (bucketItem) { bucketItem.metric.add(value); // TODO(abhi): Do we need this check? @@ -52,7 +54,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator { bucketItem.timestamp = timestamp; } } else { - bucketItem = { + this._buckets.set(bucketKey, { // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. metric: new METRIC_MAP[metricType](value), timestamp, @@ -60,13 +62,8 @@ export class BrowserMetricsAggregator implements MetricsAggregator { name, unit, tags, - }; - this._buckets.set(bucketKey, bucketItem); + }); } - - // If value is a string, it's a set metric so calculate the delta from the previous weight. - const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; - updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); } /** diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts deleted file mode 100644 index dff610574b2c..000000000000 --- a/packages/core/src/metrics/metric-summary.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { MeasurementUnit, Span } from '@sentry/types'; -import type { MetricSummary } from '@sentry/types'; -import type { Primitive } from '@sentry/types'; -import { dropUndefinedKeys } from '@sentry/utils'; -import { getActiveSpan } from '../tracing'; -import type { MetricType } from './types'; - -/** - * key: bucketKey - * value: [exportKey, MetricSummary] - */ -type MetricSummaryStorage = Map; - -let SPAN_METRIC_SUMMARY: WeakMap | undefined; - -function getMetricStorageForSpan(span: Span): MetricSummaryStorage | undefined { - return SPAN_METRIC_SUMMARY ? SPAN_METRIC_SUMMARY.get(span) : undefined; -} - -/** - * Fetches the metric summary if it exists for the passed span - */ -export function getMetricSummaryJsonForSpan(span: Span): Record | undefined { - const storage = getMetricStorageForSpan(span); - - if (!storage) { - return undefined; - } - const output: Record = {}; - - for (const [, [exportKey, summary]] of storage) { - output[exportKey] = dropUndefinedKeys(summary); - } - - return output; -} - -/** - * Updates the metric summary on the currently active span - */ -export function updateMetricSummaryOnActiveSpan( - metricType: MetricType, - sanitizedName: string, - value: number, - unit: MeasurementUnit, - tags: Record, - bucketKey: string, -): void { - const span = getActiveSpan(); - if (span) { - const storage = getMetricStorageForSpan(span) || new Map(); - - const exportKey = `${metricType}:${sanitizedName}@${unit}`; - const bucketItem = storage.get(bucketKey); - - if (bucketItem) { - const [, summary] = bucketItem; - storage.set(bucketKey, [ - exportKey, - { - min: Math.min(summary.min, value), - max: Math.max(summary.max, value), - count: (summary.count += 1), - sum: (summary.sum += value), - tags: summary.tags, - }, - ]); - } else { - storage.set(bucketKey, [ - exportKey, - { - min: value, - max: value, - count: 1, - sum: value, - tags, - }, - ]); - } - - if (!SPAN_METRIC_SUMMARY) { - SPAN_METRIC_SUMMARY = new WeakMap(); - } - - SPAN_METRIC_SUMMARY.set(span, storage); - } -} diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 1e12628ae2a1..165677455d7f 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -16,7 +16,6 @@ import type { import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import { getRootSpan } from '../utils/getRootSpan'; import { @@ -625,7 +624,6 @@ export class Span implements SpanInterface { timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, - _metrics_summary: getMetricSummaryJsonForSpan(this), }); } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 709aa628f42e..026723929471 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -15,7 +15,6 @@ import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; -import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; @@ -332,7 +331,6 @@ export class Transaction extends SpanClass implements TransactionInterface { capturedSpanIsolationScope, dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, - _metrics_summary: getMetricSummaryJsonForSpan(this), ...(source && { transaction_info: { source, diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 9e16100bbb1b..50322f18fbc6 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -11,7 +11,7 @@ import type { Request } from './request'; import type { CaptureContext } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { Severity, SeverityLevel } from './severity'; -import type { MetricSummary, Span, SpanJSON } from './span'; +import type { Span, SpanJSON } from './span'; import type { Thread } from './thread'; import type { TransactionSource } from './transaction'; import type { User } from './user'; @@ -73,7 +73,6 @@ export interface ErrorEvent extends Event { } export interface TransactionEvent extends Event { type: 'transaction'; - _metrics_summary?: Record; } /** JSDoc */ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d4fcd439ae4a..5970383febc3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -99,7 +99,6 @@ export type { SpanJSON, SpanContextData, TraceFlag, - MetricSummary, } from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; @@ -151,9 +150,5 @@ export type { export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions'; export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin'; -export type { - MetricsAggregator, - MetricBucketItem, - MetricInstance, -} from './metrics'; +export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics'; export type { ParameterizedString } from './parameterize'; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 0743497f1411..73c2fbdaaaa8 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -31,14 +31,6 @@ export type SpanAttributes = Partial<{ }> & Record; -export type MetricSummary = { - min: number; - max: number; - count: number; - sum: number; - tags?: Record | undefined; -}; - /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; @@ -55,7 +47,6 @@ export interface SpanJSON { timestamp?: number; trace_id: string; origin?: SpanOrigin; - _metrics_summary?: Record; } // These are aligned with OpenTelemetry trace flags From 32e72a1e2b55c3efc528c7efc49b610b9ba3a130 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 5 Feb 2024 21:27:10 +0100 Subject: [PATCH 56/68] fix(nextjs): Fix navigation tracing on app router (#10502) --- .../tests/client-app-routing-instrumentation.test.ts | 4 ++-- .../nextjs/src/common/wrapGenerationFunctionWithSentry.ts | 2 ++ packages/nextjs/src/common/wrapServerComponentWithSentry.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) 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 9c6dd31496a8..d52cd4f18893 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 @@ -37,7 +37,7 @@ test('Creates a navigation transaction for app router routes', async ({ page }) ); }); - const servercomponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])' && (await clientNavigationTransactionPromise).contexts?.trace?.trace_id === @@ -48,5 +48,5 @@ test('Creates a navigation transaction for app router routes', async ({ page }) await page.getByText('/server-component/parameter/foo/bar/baz').click(); expect(await clientNavigationTransactionPromise).toBeDefined(); - expect(await servercomponentTransactionPromise).toBeDefined(); + expect(await serverComponentTransactionPromise).toBeDefined(); }); diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 276cbec81d35..d1dbecbaec63 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -3,6 +3,7 @@ import { addTracingExtensions, captureException, getClient, + getCurrentScope, handleCallbackErrors, startSpanManual, withIsolationScope, @@ -59,6 +60,7 @@ export function wrapGenerationFunctionWithSentry a const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); isolationScope.setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); return startSpanManual( { diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 6d6e7758bbf9..de0c1da9c1f9 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -2,6 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, + getCurrentScope, handleCallbackErrors, startSpanManual, withIsolationScope, @@ -51,6 +52,7 @@ export function wrapServerComponentWithSentry any> const propagationContext = commonObjectToPropagationContext(context.headers, incomingPropagationContext); isolationScope.setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); return startSpanManual( { From 05fb074b21714d90a54b951b7c4fa9559f2d7b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Mon, 5 Feb 2024 21:34:23 +0100 Subject: [PATCH 57/68] fix(docs): Fix package names for contextLinesIntegration in migration guide (#10503) Co-authored-by: Luca Forstner --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 1f157fc735cc..4006ae6e1c3f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -60,7 +60,7 @@ The following list shows how integrations should be migrated: | `new RewriteFrames()` | `rewriteFramesIntegration()` | `@sentry/integrations` | | `new SessionTiming()` | `sessionTimingIntegration()` | `@sentry/integrations` | | `new HttpClient()` | `httpClientIntegration()` | `@sentry/integrations` | -| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser`, `@sentry/node`, `@sentry/deno` | +| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/integrations`, `@sentry/node`, `@sentry/deno`, `@sentry/bun` | | `new Breadcrumbs()` | `breadcrumbsIntegration()` | `@sentry/browser`, `@sentry/deno` | | `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` , `@sentry/deno` | | `new HttpContext()` | `httpContextIntegration()` | `@sentry/browser` | From 1a4b6e64618ec43ea6fe86f66643953e9026980b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 6 Feb 2024 08:43:48 +0100 Subject: [PATCH 58/68] build(ci): Bump action dependencies for node profiling (#10473) Noticed warnings in GH for these, so bumping all of these to latest. --- .github/workflows/build.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8977d691322..b8afc90dbde9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -566,6 +566,8 @@ jobs: with: node-version: 20 - uses: actions/setup-python@v5 + with: + python-version: '3.11.7' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -1346,12 +1348,12 @@ jobs: ln -sf python3 /usr/bin/python - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: ${{ env.HEAD_COMMIT }} - name: Restore dependency cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 id: restore-dependencies with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} @@ -1359,7 +1361,7 @@ jobs: enableCrossOsArchive: true - name: Restore build cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 id: restore-build with: path: ${{ env.CACHED_BUILD_PATHS }} @@ -1378,14 +1380,14 @@ jobs: run: yarn config set network-timeout 600000 -g - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 if: ${{ !contains(matrix.container, 'alpine') }} id: python-setup with: python-version: '3.8.10' - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} From f47d11f93b3f3957b1899a48f0b569afaf2a9d81 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 6 Feb 2024 11:41:45 +0100 Subject: [PATCH 59/68] feat(browser): Deprecate `BrowserTracing` integration (#10493) There is a proper replacement for all of them now. Depends on: * https://github.com/getsentry/sentry-javascript/pull/10491 * https://github.com/getsentry/sentry-javascript/pull/10489 * https://github.com/getsentry/sentry-javascript/pull/10488 --- MIGRATION.md | 121 +++++++++++++++++- packages/astro/src/client/sdk.ts | 4 +- packages/astro/test/client/sdk.test.ts | 4 +- packages/browser/src/helpers.ts | 2 + packages/browser/src/index.bundle.feedback.ts | 2 + packages/browser/src/index.bundle.replay.ts | 2 + .../index.bundle.tracing.replay.feedback.ts | 2 + .../src/index.bundle.tracing.replay.ts | 2 + packages/browser/src/index.bundle.tracing.ts | 2 + packages/browser/src/index.bundle.ts | 2 + packages/browser/src/index.ts | 1 + .../deno/src/integrations/globalhandlers.ts | 2 +- .../sentry-performance.ts | 11 +- packages/ember/addon/types.ts | 6 +- packages/gatsby/src/utils/integrations.ts | 6 +- packages/gatsby/test/gatsby-browser.test.ts | 4 +- packages/gatsby/test/sdk.test.ts | 10 +- .../integration-shims/src/BrowserTracing.ts | 10 +- packages/integration-shims/src/Feedback.ts | 2 +- .../src/client/browserTracingIntegration.ts | 2 + .../src/client/browserTracingIntegration.ts | 2 + packages/sveltekit/test/client/sdk.test.ts | 3 + .../src/browser/browsertracing.ts | 2 + .../tracing-internal/src/browser/index.ts | 6 +- packages/tracing-internal/src/index.ts | 1 + packages/tracing/src/index.ts | 2 + 26 files changed, 190 insertions(+), 23 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 4006ae6e1c3f..e315bf77ccfd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -10,6 +10,122 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Depreacted `BrowserTracing` integration + +The `BrowserTracing` integration, together with the custom routing instrumentations passed to it, are deprecated in v8. +Instead, you should use `Sentry.browserTracingIntegration()`. + +Package-specific browser tracing integrations are available directly. In most cases, there is a single integration +provided for each package, which will make sure to set up performance tracing correctly for the given SDK. For react, we +provide multiple integrations to cover different router integrations: + +### `@sentry/browser`, `@sentry/svelte`, `@sentry/gatsby` + +```js +import * as Sentry from '@sentry/browser'; + +Sentry.init({ + integrations: [Sentry.browserTracingIntegration()], +}); +``` + +### `@sentry/react` + +```js +import * as Sentry from '@sentry/react'; + +Sentry.init({ + integrations: [ + // No react router + Sentry.browserTracingIntegration(), + // OR, if you are using react router, instead use one of the following: + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + stripBasename, + }), + Sentry.reactRouterV5BrowserTracingIntegration({ + history, + }), + Sentry.reactRouterV4BrowserTracingIntegration({ + history, + }), + Sentry.reactRouterV3BrowserTracingIntegration({ + history, + routes, + match, + }), + ], +}); +``` + +### `@sentry/vue` + +```js +import * as Sentry from '@sentry/vue'; + +Sentry.init({ + integrations: [ + Sentry.browserTracingIntegration({ + // pass router in, if applicable + router, + }), + ], +}); +``` + +### `@sentry/angular` & `@sentry/angular-ivy` + +```js +import * as Sentry from '@sentry/angular'; + +Sentry.init({ + integrations: [Sentry.browserTracingIntegration()], +}); + +// You still need to add the Trace Service like before! +``` + +### `@sentry/remix` + +```js +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + integrations: [ + Sentry.browserTracingIntegration({ + useEffect, + useLocation, + useMatches, + }), + ], +}); +``` + +### `@sentry/nextjs`, `@sentry/astro`, `@sentry/sveltekit` + +Browser tracing is automatically set up for you in these packages. If you need to customize the options, you can do it +like this: + +```js +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + integrations: [ + Sentry.browserTracingIntegration({ + // add custom options here + }), + ], +}); +``` + +### `@sentry/ember` + +Browser tracing is automatically set up for you. You can configure it as before through configuration. + ## Deprecated `transactionContext` passed to `tracesSampler` Instead of an `transactionContext` being passed to the `tracesSampler` callback, the callback will directly receive @@ -43,6 +159,7 @@ The following list shows how integrations should be migrated: | Old | New | Packages | | ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `new BrowserTracing()` | `browserTracingIntegration()` | `@sentry/browser` | | `new InboundFilters()` | `inboundFiltersIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | | `new FunctionToString()` | `functionToStringIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | | `new LinkedErrors()` | `linkedErrorsIntegration()` | `@sentry/core`, `@sentry/browser`, `@sentry/node`, `@sentry/deno`, `@sentry/bun`, `@sentry/vercel-edge` | @@ -75,8 +192,8 @@ The following list shows how integrations should be migrated: | `new OnUncaughtException()` | `onUncaughtExceptionIntegration()` | `@sentry/node` | | `new OnUnhandledRejection()` | `onUnhandledRejectionIntegration()` | `@sentry/node` | | `new LocalVariables()` | `localVariablesIntegration()` | `@sentry/node` | -| `new Spotlight()` | `spotlightIntergation()` | `@sentry/node` | -| `new Anr()` | `anrIntergation()` | `@sentry/node` | +| `new Spotlight()` | `spotlightIntegration()` | `@sentry/node` | +| `new Anr()` | `anrIntegration()` | `@sentry/node` | | `new Hapi()` | `hapiIntegration()` | `@sentry/node` | | `new Undici()` | `nativeNodeFetchIntegration()` | `@sentry/node` | | `new Http()` | `httpIntegration()` | `@sentry/node` | diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts index 8d2b70ee6751..a289296c2ab2 100644 --- a/packages/astro/src/client/sdk.ts +++ b/packages/astro/src/client/sdk.ts @@ -1,6 +1,6 @@ import type { BrowserOptions } from '@sentry/browser'; import { - BrowserTracing, + browserTracingIntegration, getDefaultIntegrations as getBrowserDefaultIntegrations, init as initBrowserSdk, setTag, @@ -34,7 +34,7 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] | undefi // in which case everything inside will get treeshaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { if (hasTracingEnabled(options)) { - return [...getBrowserDefaultIntegrations(options), new BrowserTracing()]; + return [...getBrowserDefaultIntegrations(options), browserTracingIntegration()]; } } diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 3960c25eccd3..d6f22dc9ed7a 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -104,12 +104,14 @@ describe('Sentry client SDK', () => { it('Overrides the automatically default BrowserTracing instance with a a user-provided BrowserTracing instance', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', + // eslint-disable-next-line deprecation/deprecation integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], enableTracing: true, }); const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations; + // eslint-disable-next-line deprecation/deprecation const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; const options = browserTracing.options; @@ -120,7 +122,7 @@ describe('Sentry client SDK', () => { expect(options.finalTimeout).toEqual(10); }); - it('Overrides the automatically default BrowserTracing instance with a a user-provided browserTracingIntergation instance', () => { + it('Overrides the automatically default BrowserTracing instance with a a user-provided browserTracingIntegration instance', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 5fff014eaa8d..35930167672d 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -201,6 +201,7 @@ export function bundleBrowserTracingIntegration( options: Parameters[0] = {}, ): Integration { // Migrate some options from the old integration to the new one + // eslint-disable-next-line deprecation/deprecation const opts: ConstructorParameters[0] = options; if (typeof options.markBackgroundSpan === 'boolean') { @@ -215,5 +216,6 @@ export function bundleBrowserTracingIntegration( opts.startTransactionOnLocationChange = options.instrumentNavigation; } + // eslint-disable-next-line deprecation/deprecation return new BrowserTracing(opts); } diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index af4de5ea063d..8e653c2d4757 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -14,10 +14,12 @@ import * as Sentry from './index.bundle.base'; // eslint-disable-next-line deprecation/deprecation Sentry.Integrations.Replay = Replay; +// eslint-disable-next-line deprecation/deprecation Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { + // eslint-disable-next-line deprecation/deprecation BrowserTracing, browserTracingIntegration, addTracingExtensions, diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts index 175a435fadcf..2e4619ab49ea 100644 --- a/packages/browser/src/index.bundle.replay.ts +++ b/packages/browser/src/index.bundle.replay.ts @@ -14,10 +14,12 @@ import * as Sentry from './index.bundle.base'; // eslint-disable-next-line deprecation/deprecation Sentry.Integrations.Replay = Replay; +// eslint-disable-next-line deprecation/deprecation Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { + // eslint-disable-next-line deprecation/deprecation BrowserTracing, browserTracingIntegration, addTracingExtensions, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index df151bba0a8f..4a015f0dd9fe 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -11,6 +11,7 @@ import * as Sentry from './index.bundle.base'; // eslint-disable-next-line deprecation/deprecation Sentry.Integrations.Replay = Replay; +// eslint-disable-next-line deprecation/deprecation Sentry.Integrations.BrowserTracing = BrowserTracing; // We are patching the global object with our hub extension methods @@ -23,6 +24,7 @@ export { Replay, feedbackIntegration, replayIntegration, + // eslint-disable-next-line deprecation/deprecation BrowserTracing, browserTracingIntegration, Span, diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 2437a8546d5c..c880f97bd84f 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -11,6 +11,7 @@ import * as Sentry from './index.bundle.base'; // eslint-disable-next-line deprecation/deprecation Sentry.Integrations.Replay = Replay; +// eslint-disable-next-line deprecation/deprecation Sentry.Integrations.BrowserTracing = BrowserTracing; // We are patching the global object with our hub extension methods @@ -23,6 +24,7 @@ export { Replay, replayIntegration, feedbackIntegration, + // eslint-disable-next-line deprecation/deprecation BrowserTracing, browserTracingIntegration, Span, diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index 2ca0613146f0..e645de683dd5 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -11,6 +11,7 @@ import * as Sentry from './index.bundle.base'; // eslint-disable-next-line deprecation/deprecation Sentry.Integrations.Replay = Replay; +// eslint-disable-next-line deprecation/deprecation Sentry.Integrations.BrowserTracing = BrowserTracing; // We are patching the global object with our hub extension methods @@ -23,6 +24,7 @@ export { Replay, feedbackIntegration, replayIntegration, + // eslint-disable-next-line deprecation/deprecation BrowserTracing, browserTracingIntegration, Span, diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts index 93a0b0cb498a..3087d7d317ca 100644 --- a/packages/browser/src/index.bundle.ts +++ b/packages/browser/src/index.bundle.ts @@ -15,10 +15,12 @@ import * as Sentry from './index.bundle.base'; // eslint-disable-next-line deprecation/deprecation Sentry.Integrations.Replay = Replay; +// eslint-disable-next-line deprecation/deprecation Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { + // eslint-disable-next-line deprecation/deprecation BrowserTracing, addTracingExtensions, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 4518f0174f35..2be5c71c4518 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -54,6 +54,7 @@ export { } from '@sentry-internal/feedback'; export { + // eslint-disable-next-line deprecation/deprecation BrowserTracing, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 0c830c40da25..2c562a7aa0f9 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -48,7 +48,7 @@ export const globalHandlersIntegration = defineIntegration(_globalHandlersIntegr /** * Global handlers. - * @deprecated Use `globalHandlersIntergation()` instead. + * @deprecated Use `globalHandlersIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation export const GlobalHandlers = convertIntegrationFnToClass( diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index c86e280d167b..f4c47998ea90 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -109,7 +109,11 @@ export function _instrumentEmberRouter( return; } - if (url && browserTracingOptions.startTransactionOnPageLoad !== false) { + if ( + url && + browserTracingOptions.startTransactionOnPageLoad !== false && + browserTracingOptions.instrumentPageLoad !== false + ) { const routeInfo = routerService.recognize(url); Sentry.startBrowserTracingPageLoadSpan(client, { name: `route:${routeInfo.name}`, @@ -132,7 +136,10 @@ export function _instrumentEmberRouter( getBackburner().off('end', finishActiveTransaction); }; - if (browserTracingOptions.startTransactionOnLocationChange === false) { + if ( + browserTracingOptions.startTransactionOnLocationChange === false && + browserTracingOptions.instrumentNavigation === false + ) { return; } diff --git a/packages/ember/addon/types.ts b/packages/ember/addon/types.ts index 868d6265f62d..bf333740ee5b 100644 --- a/packages/ember/addon/types.ts +++ b/packages/ember/addon/types.ts @@ -1,7 +1,9 @@ -import type { BrowserOptions, BrowserTracing } from '@sentry/browser'; +import type { BrowserOptions, BrowserTracing, browserTracingIntegration } from '@sentry/browser'; import type { Transaction, TransactionContext } from '@sentry/types'; -type BrowserTracingOptions = ConstructorParameters[0]; +type BrowserTracingOptions = Parameters[0] & + // eslint-disable-next-line deprecation/deprecation + ConstructorParameters[0]; export type EmberSentryConfig = { sentry: BrowserOptions & { browserTracingOptions?: BrowserTracingOptions }; diff --git a/packages/gatsby/src/utils/integrations.ts b/packages/gatsby/src/utils/integrations.ts index 94ef28f21272..7c61adee1d50 100644 --- a/packages/gatsby/src/utils/integrations.ts +++ b/packages/gatsby/src/utils/integrations.ts @@ -1,5 +1,5 @@ import { hasTracingEnabled } from '@sentry/core'; -import { BrowserTracing } from '@sentry/react'; +import { browserTracingIntegration } from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { GatsbyOptions } from './types'; @@ -31,8 +31,8 @@ export function getIntegrationsFromOptions(options: GatsbyOptions): UserIntegrat * @param isTracingEnabled Whether the user has enabled tracing. */ function getIntegrationsFromArray(userIntegrations: Integration[], isTracingEnabled: boolean): Integration[] { - if (isTracingEnabled && !userIntegrations.some(integration => integration.name === BrowserTracing.name)) { - userIntegrations.push(new BrowserTracing()); + if (isTracingEnabled && !userIntegrations.some(integration => integration.name === 'BrowserTracing')) { + userIntegrations.push(browserTracingIntegration()); } return userIntegrations; } diff --git a/packages/gatsby/test/gatsby-browser.test.ts b/packages/gatsby/test/gatsby-browser.test.ts index b67305042c71..cf456a9d3e9d 100644 --- a/packages/gatsby/test/gatsby-browser.test.ts +++ b/packages/gatsby/test/gatsby-browser.test.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { onClientEntry } from '../gatsby-browser'; -import { BrowserTracing } from '../src/index'; +import { browserTracingIntegration } from '../src/index'; (global as any).__SENTRY_RELEASE__ = '683f3a6ab819d47d23abfca9a914c81f0524d35b'; (global as any).__SENTRY_DSN__ = 'https://examplePublicKey@o0.ingest.sentry.io/0'; @@ -141,7 +141,7 @@ describe('onClientEntry', () => { }); it('only defines a single `BrowserTracing` integration', () => { - const integrations = [new BrowserTracing()]; + const integrations = [browserTracingIntegration()]; onClientEntry(undefined, { tracesSampleRate: 0.5, integrations }); expect(sentryInit).toHaveBeenLastCalledWith( diff --git a/packages/gatsby/test/sdk.test.ts b/packages/gatsby/test/sdk.test.ts index c3c95cdabc1f..28206d1ef6c5 100644 --- a/packages/gatsby/test/sdk.test.ts +++ b/packages/gatsby/test/sdk.test.ts @@ -1,4 +1,4 @@ -import { BrowserTracing, SDK_VERSION, init } from '@sentry/react'; +import { SDK_VERSION, browserTracingIntegration, init } from '@sentry/react'; import type { Integration } from '@sentry/types'; import { init as gatsbyInit } from '../src/sdk'; @@ -68,27 +68,27 @@ describe('Integrations from options', () => { [ 'tracing disabled, with BrowserTracing as an array', [], - { integrations: [new BrowserTracing()] }, + { integrations: [browserTracingIntegration()] }, ['BrowserTracing'], ], [ 'tracing disabled, with BrowserTracing as a function', [], { - integrations: () => [new BrowserTracing()], + integrations: () => [browserTracingIntegration()], }, ['BrowserTracing'], ], [ 'tracing enabled, with BrowserTracing as an array', [], - { tracesSampleRate: 1, integrations: [new BrowserTracing()] }, + { tracesSampleRate: 1, integrations: [browserTracingIntegration()] }, ['BrowserTracing'], ], [ 'tracing enabled, with BrowserTracing as a function', [], - { tracesSampleRate: 1, integrations: () => [new BrowserTracing()] }, + { tracesSampleRate: 1, integrations: () => [browserTracingIntegration()] }, ['BrowserTracing'], ], ] as TestArgs[])( diff --git a/packages/integration-shims/src/BrowserTracing.ts b/packages/integration-shims/src/BrowserTracing.ts index 8e3d61bae58f..1c68faf30469 100644 --- a/packages/integration-shims/src/BrowserTracing.ts +++ b/packages/integration-shims/src/BrowserTracing.ts @@ -5,6 +5,8 @@ import { consoleSandbox } from '@sentry/utils'; * This is a shim for the BrowserTracing integration. * It is needed in order for the CDN bundles to continue working when users add/remove tracing * from it, without changing their config. This is necessary for the loader mechanism. + * + * @deprecated Use `browserTracingIntegration()` instead. */ class BrowserTracingShim implements Integration { /** @@ -19,6 +21,7 @@ class BrowserTracingShim implements Integration { // eslint-disable-next-line @typescript-eslint/no-explicit-any public constructor(_options: any) { + // eslint-disable-next-line deprecation/deprecation this.name = BrowserTracingShim.id; consoleSandbox(() => { @@ -39,10 +42,15 @@ class BrowserTracingShim implements Integration { * from it, without changing their config. This is necessary for the loader mechanism. */ function browserTracingIntegrationShim(_options: unknown): Integration { + // eslint-disable-next-line deprecation/deprecation return new BrowserTracingShim({}); } -export { BrowserTracingShim as BrowserTracing, browserTracingIntegrationShim as browserTracingIntegration }; +export { + // eslint-disable-next-line deprecation/deprecation + BrowserTracingShim as BrowserTracing, + browserTracingIntegrationShim as browserTracingIntegration, +}; /** Shim function */ export function addTracingExtensions(): void { diff --git a/packages/integration-shims/src/Feedback.ts b/packages/integration-shims/src/Feedback.ts index 232e2be0830b..7b717e3a4e3b 100644 --- a/packages/integration-shims/src/Feedback.ts +++ b/packages/integration-shims/src/Feedback.ts @@ -6,7 +6,7 @@ import { consoleSandbox } from '@sentry/utils'; * It is needed in order for the CDN bundles to continue working when users add/remove feedback * from it, without changing their config. This is necessary for the loader mechanism. * - * @deprecated Use `feedbackIntergation()` instead. + * @deprecated Use `feedbackIntegration()` instead. */ class FeedbackShim implements Integration { /** diff --git a/packages/nextjs/src/client/browserTracingIntegration.ts b/packages/nextjs/src/client/browserTracingIntegration.ts index bf62725e105b..af8f59f53b6f 100644 --- a/packages/nextjs/src/client/browserTracingIntegration.ts +++ b/packages/nextjs/src/client/browserTracingIntegration.ts @@ -13,7 +13,9 @@ import { nextRouterInstrumentation } from '../index.client'; * * @deprecated Use `browserTracingIntegration` instead. */ +// eslint-disable-next-line deprecation/deprecation export class BrowserTracing extends OriginalBrowserTracing { + // eslint-disable-next-line deprecation/deprecation public constructor(options?: ConstructorParameters[0]) { super({ // eslint-disable-next-line deprecation/deprecation diff --git a/packages/sveltekit/src/client/browserTracingIntegration.ts b/packages/sveltekit/src/client/browserTracingIntegration.ts index ffabc2a374a7..5e80cdc92f28 100644 --- a/packages/sveltekit/src/client/browserTracingIntegration.ts +++ b/packages/sveltekit/src/client/browserTracingIntegration.ts @@ -19,7 +19,9 @@ import { svelteKitRoutingInstrumentation } from './router'; * includes SvelteKit-specific routing instrumentation out of the box. Therefore there's no need * to pass in `svelteKitRoutingInstrumentation` anymore. */ +// eslint-disable-next-line deprecation/deprecation export class BrowserTracing extends OriginalBrowserTracing { + // eslint-disable-next-line deprecation/deprecation public constructor(options?: ConstructorParameters[0]) { super({ // eslint-disable-next-line deprecation/deprecation diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index c6eca47e5e79..bc863af99897 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -98,10 +98,12 @@ describe('Sentry client SDK', () => { it('Merges a user-provided BrowserTracing integration with the automatically added one', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', + // eslint-disable-next-line deprecation/deprecation integrations: [new BrowserTracing({ finalTimeout: 10 })], enableTracing: true, }); + // eslint-disable-next-line deprecation/deprecation const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; const options = browserTracing.options; @@ -122,6 +124,7 @@ describe('Sentry client SDK', () => { enableTracing: true, }); + // eslint-disable-next-line deprecation/deprecation const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; const options = browserTracing.options; diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index afe100327d59..e2b1c120ccd5 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -156,6 +156,8 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { * * The integration can be configured with a variety of options, and can be extended to use * any routing library. This integration uses {@see IdleTransaction} to create transactions. + * + * @deprecated Use `browserTracingIntegration()` instead. */ export class BrowserTracing implements Integration { // This class currently doesn't have a static `id` field like the other integration classes, because it prevented diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 7d29d2f11e9e..a1619d4ea8ae 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -2,7 +2,11 @@ export * from '../exports'; export type { RequestInstrumentationOptions } from './request'; -export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing'; +export { + // eslint-disable-next-line deprecation/deprecation + BrowserTracing, + BROWSER_TRACING_INTEGRATION_ID, +} from './browsertracing'; export { browserTracingIntegration, startBrowserTracingNavigationSpan, diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts index 233baae9c308..4f09ca6a2e96 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -13,6 +13,7 @@ export { export type { LazyLoadedIntegration } from './node'; export { + // eslint-disable-next-line deprecation/deprecation BrowserTracing, browserTracingIntegration, startBrowserTracingNavigationSpan, diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index d789a2b68520..24b1241af764 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -38,6 +38,7 @@ import { * import { BrowserTracing } from '@sentry/browser'; * new BrowserTracing() */ +// eslint-disable-next-line deprecation/deprecation export const BrowserTracing = BrowserTracingT; // BrowserTracing is already exported as part of `Integrations` below (and for the moment will remain so for @@ -50,6 +51,7 @@ export const BrowserTracing = BrowserTracingT; * import { BrowserTracing } from '@sentry/browser'; * new BrowserTracing() */ +// eslint-disable-next-line deprecation/deprecation export type BrowserTracing = BrowserTracingT; /** From 01deeacafaa7172100dc695816a214a13e15e1dc Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 6 Feb 2024 11:43:24 +0100 Subject: [PATCH 60/68] build(ci): Make dependabot actually update the sentry-cli versions (#10509) Currently, it is only updating the yarn.lock, which is not really what we want, we want the version in the package.json to be raised, which is what this does (I believe). See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#versioning-strategy See https://github.com/getsentry/sentry-javascript/pull/10496 for how that does not correctly bump right now. --- .github/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 43d75c60ba14..6a938d15facc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,7 @@ updates: allow: - dependency-name: "@sentry/cli" - dependency-name: "@sentry/vite-plugin" + versioning-strategy: increase commit-message: prefix: feat prefix-development: feat From fdb1a5c7e2061a74b0fac290b38c8fa3e80db4d1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 6 Feb 2024 11:51:22 +0100 Subject: [PATCH 61/68] fix(nextjs): Apply server action data to correct isolation scope (#10514) --- .../nextjs-app-dir/tests/transactions.test.ts | 10 +++++----- .../src/common/withServerActionInstrumentation.ts | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) 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 6c99733381b6..3e146433defc 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 @@ -130,11 +130,11 @@ test('Should send a transaction for instrumented server actions', async ({ page await page.getByText('Run Action').click(); expect(await serverComponentTransactionPromise).toBeDefined(); - expect( - (await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data.some-text-value'], - ).toEqual('some-default-value'); - expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_result']).toEqual({ - city: 'Vienna', + expect((await serverComponentTransactionPromise).extra).toMatchObject({ + 'server_action_form_data.some-text-value': 'some-default-value', + server_action_result: { + city: 'Vienna', + }, }); expect(Object.keys((await serverComponentTransactionPromise).request?.headers || {}).length).toBeGreaterThan(0); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 2de1497b2dfd..1f4cdaf992c9 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -1,3 +1,4 @@ +import { getIsolationScope } from '@sentry/core'; import { addTracingExtensions, captureException, @@ -115,12 +116,12 @@ async function withServerActionInstrumentationImplementation
{ - isolationScope.setExtra( + getIsolationScope().setExtra( `server_action_form_data.${key}`, typeof value === 'string' ? value : '[non-string value]', ); From c8400c731653375963125c5a69abe676177a01ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:58:15 +0100 Subject: [PATCH 62/68] ci(deps): bump hmarr/auto-approve-action from 3 to 4 (#10518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hmarr/auto-approve-action](https://github.com/hmarr/auto-approve-action) from 3 to 4.
Release notes

Sourced from hmarr/auto-approve-action's releases.

v4.0.0

What's Changed

  • Upgrade from node 16 to node 20
  • Upgrade dependencies and switch from nock to msw for API mocking

Full Changelog: https://github.com/hmarr/auto-approve-action/compare/v3.2.1...v4.0.0

v3.2.1

What's Changed

Full Changelog: https://github.com/hmarr/auto-approve-action/compare/v3.2.0...v3.2.1

v3.2.0

What's Changed

Full Changelog: https://github.com/hmarr/auto-approve-action/compare/v3.1.0...v3.2.0

v3.1.0

What's Changed

New Contributors

Full Changelog: https://github.com/hmarr/auto-approve-action/compare/v3.0.0...v3.1.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hmarr/auto-approve-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/gitflow-sync-develop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gitflow-sync-develop.yml b/.github/workflows/gitflow-sync-develop.yml index ca7f27d1f5f9..612d2802a580 100644 --- a/.github/workflows/gitflow-sync-develop.yml +++ b/.github/workflows/gitflow-sync-develop.yml @@ -47,7 +47,7 @@ jobs: # https://github.com/marketplace/actions/auto-approve - name: Auto approve PR if: steps.open-pr.outputs.pr_number != '' - uses: hmarr/auto-approve-action@v3 + uses: hmarr/auto-approve-action@v4 with: pull-request-number: ${{ steps.open-pr.outputs.pr_number }} review-message: 'Auto approved automated PR' From c7228027be3f8cc609219d995c2cbe575283f537 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:58:52 +0100 Subject: [PATCH 63/68] ci(deps): bump mshick/add-pr-comment from a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 to dd126dd8c253650d181ad9538d8b4fa218fc31e8 (#10516) Bumps [mshick/add-pr-comment](https://github.com/mshick/add-pr-comment) from a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 to dd126dd8c253650d181ad9538d8b4fa218fc31e8.
Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8afc90dbde9..a8040f77501d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -217,7 +217,7 @@ jobs: pull-requests: write steps: - name: PR is opened against master - uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + uses: mshick/add-pr-comment@dd126dd8c253650d181ad9538d8b4fa218fc31e8 if: ${{ github.base_ref == 'master' && !startsWith(github.head_ref, 'prepare-release/') }} with: message: | From 709ab2fc1b6ca42b5b148a16223aeac4e36fed17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:02:58 +0100 Subject: [PATCH 64/68] ci(deps): bump actions/download-artifact from 3 to 4 (#10517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
Release notes

Sourced from actions/download-artifact's releases.

v4.0.0

What's Changed

The release of upload-artifact@v4 and download-artifact@v4 are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.

ℹ️ However, this is a major update that includes breaking changes. Artifacts created with versions v3 and below are not compatible with the v4 actions. Uploads and downloads must use the same major actions versions. There are also key differences from previous versions that may require updates to your workflows.

For more information, please see:

  1. The changelog post.
  2. The README.
  3. The migration documentation.
  4. As well as the underlying npm package, @​actions/artifact documentation.

New Contributors

Full Changelog: https://github.com/actions/download-artifact/compare/v3...v4.0.0

v3.0.2

  • Bump @actions/artifact to v1.1.1 - actions/download-artifact#195
  • Fixed a bug in Node16 where if an HTTP download finished too quickly (<1ms, e.g. when it's mocked) we attempt to delete a temp file that has not been created yet actions/toolkit#1278

v3.0.1

Commits
  • eaceaf8 Merge pull request #291 from actions/eggyhead/update-artifact-v2.1.1
  • 81eafdc update artifact license
  • 9ac5cad updating artifact dependency to version 2.1.1
  • 3ad8411 Merge pull request #287 from actions/robherley/sync-migration-docs
  • 1de4643 Sync migration docs with upload-artifact
  • bb3fa7f Merge pull request #275 from actions/robherley/better-log-msgs
  • a244de5 ncc
  • 355659b clarify log messages when using pattern/merge-multiple params
  • 6b208ae Merge pull request #274 from actions/vmjoseph/timeout-patch
  • 6c5b580 only adding updated license
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8040f77501d..17045f843c7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -408,7 +408,7 @@ jobs: run: yarn build:tarball --ignore @sentry/profiling-node - name: Restore profiling tarball - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: profiling-node-tarball-${{ github.sha }} path: packages/profiling-node @@ -995,7 +995,7 @@ jobs: if: | (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (github.event_name != 'pull_request') - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: profiling-node-binaries-${{ github.sha }} path: ${{ github.workspace }}/packages/profiling-node/lib/ @@ -1112,7 +1112,7 @@ jobs: if: | (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (github.event_name != 'pull_request') - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: profiling-node-binaries-${{ github.sha }} path: ${{ github.workspace }}/packages/profiling-node/lib/ From 0c568361f7974e5d5c9b97634ba3804cb71c1234 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:04:21 +0000 Subject: [PATCH 65/68] ci(deps): bump actions/upload-artifact from 3 to 4 (#10515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
Release notes

Sourced from actions/upload-artifact's releases.

v4.0.0

What's Changed

The release of upload-artifact@v4 and download-artifact@v4 are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.

ℹ️ However, this is a major update that includes breaking changes. Artifacts created with versions v3 and below are not compatible with the v4 actions. Uploads and downloads must use the same major actions versions. There are also key differences from previous versions that may require updates to your workflows.

For more information, please see:

  1. The changelog post.
  2. The README.
  3. The migration documentation.
  4. As well as the underlying npm package, @​actions/artifact documentation.

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v4.0.0

v3.1.3

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v3.1.3

v3.1.2

  • Update all @actions/* NPM packages to their latest versions- #374
  • Update all dev dependencies to their most recent versions - #375

v3.1.1

  • Update actions/core package to latest version to remove set-output deprecation warning #351

v3.1.0

What's Changed

Commits
  • 5d5d22a Merge pull request #515 from actions/eggyhead/update-artifact-v2.1.1
  • f1e993d update artifact license
  • 4881bfd updating dist:
  • a30777e @​eggyhead
  • 3a80482 Merge pull request #511 from actions/robherley/migration-docs-typo
  • 9d63e3f Merge branch 'main' into robherley/migration-docs-typo
  • dfa1ab2 fix typo with v3 artifact downloads in migration guide
  • d00351b Merge pull request #509 from markmssd/patch-1
  • 707f5a7 Update limitation of 10 artifacts upload to 500
  • 26f96df Merge pull request #505 from actions/robherley/merge-artifacts
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17045f843c7b..15731e43689f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -414,7 +414,7 @@ jobs: path: packages/profiling-node - name: Archive artifacts - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4 with: name: ${{ github.sha }} path: | @@ -1235,7 +1235,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} - name: Upload results - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4 if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository with: name: ${{ steps.process.outputs.artifactName }} @@ -1472,7 +1472,7 @@ jobs: - name: Archive Binary # @TODO: v4 breaks convenient merging of same name artifacts # https://github.com/actions/upload-artifact/issues/478 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: profiling-node-binaries-${{ github.sha }} path: | From 85f31993f652e6768970c6b6d8f4168c134cc42d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 6 Feb 2024 12:37:47 +0100 Subject: [PATCH 66/68] build(ci): Ensure we run E2E tests when profiling node is skipped (#10512) With the recent change to skip the node profiling compile step when it was not changed, we implicitly also skipped the "Prepare E2E tests" job - as that is by default skipped if the dependent job is skipped. This PR changes this so that we should be running this even if that was skipped. We have to make sure to check that `build` itself was not skipped though (as then we want to _actually_ skip this). --- .github/workflows/build.yml | 11 ++++++++++- .../test-applications/vue-3/tests/performance.test.ts | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15731e43689f..2a06f8c76954 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -952,7 +952,13 @@ jobs: job_e2e_prepare: name: Prepare E2E tests - if: + # We want to run this if: + # - The build job was successful, not skipped + # - AND if the profiling node bindings were either successful or skipped + # AND if this is not a PR from a fork or dependabot + if: | + always() && needs.job_build.result == 'success' && + (needs.job_compile_bindings_profiling_node.result == 'success' || needs.job_compile_bindings_profiling_node.result == 'skipped') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] @@ -1014,7 +1020,10 @@ jobs: name: E2E ${{ matrix.label || matrix.test-application }} Test # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks # Dependabot PRs sadly also don't have access to secrets, so we skip them as well + # We need to add the `always()` check here because the previous step has this as well :( + # See: https://github.com/actions/runner/issues/2205 if: + always() && needs.job_e2e_prepare.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build, job_e2e_prepare] 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 2210c92e5dfd..dc5bd500eee3 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 @@ -53,7 +53,7 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) 'sentry.source': 'route', 'sentry.origin': 'auto.navigation.vue', 'sentry.op': 'navigation', - 'params.id': '456', + 'params.id': '123', }, op: 'navigation', origin: 'auto.navigation.vue', From 49181ad0dcf7d1c7030d8b79a1d3676b40d019e8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 6 Feb 2024 15:05:07 +0100 Subject: [PATCH 67/68] build(ci): Use v3 of artifact actions for node-profiling (#10522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I should have read the comments on why we can't update this 😬 - we should still take some time to actually refactor this to work with the new actions, but for now this is fine. Also fixes some issues with the CI script for tarball handling etc. --- .github/workflows/build.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a06f8c76954..b30882983ddb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ env: ${{ github.workspace }}/packages/utils/esm BUILD_CACHE_KEY: ${{ github.event.inputs.commit || github.sha }} - BUILD_CACHE_TARBALL_KEY: tarball-${{ github.event.inputs.commit || github.sha }} + BUILD_PROFILING_NODE_CACHE_TARBALL_KEY: profiling-node-tarball-${{ github.event.inputs.commit || github.sha }} # GH will use the first restore-key it finds that matches # So it will start by looking for one from the same branch, else take the newest one it can find elsewhere @@ -403,15 +403,15 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Profiling + - name: Pack tarballs # Profiling tarball is built separately as we assemble the precompiled binaries run: yarn build:tarball --ignore @sentry/profiling-node - name: Restore profiling tarball - uses: actions/download-artifact@v4 + uses: actions/cache/restore@v4 with: - name: profiling-node-tarball-${{ github.sha }} - path: packages/profiling-node + key: ${{ env.BUILD_PROFILING_NODE_CACHE_TARBALL_KEY }} + path: ${{ github.workspace }}/packages/*/*.tgz - name: Archive artifacts uses: actions/upload-artifact@v4 @@ -1001,7 +1001,7 @@ jobs: if: | (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (github.event_name != 'pull_request') - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: name: profiling-node-binaries-${{ github.sha }} path: ${{ github.workspace }}/packages/profiling-node/lib/ @@ -1014,7 +1014,7 @@ jobs: uses: actions/cache/save@v4 with: path: ${{ github.workspace }}/packages/*/*.tgz - key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + key: ${{ env.BUILD_PROFILING_NODE_CACHE_TARBALL_KEY }} job_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test @@ -1121,7 +1121,7 @@ jobs: if: | (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (github.event_name != 'pull_request') - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: name: profiling-node-binaries-${{ github.sha }} path: ${{ github.workspace }}/packages/profiling-node/lib/ @@ -1139,7 +1139,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ${{ github.workspace }}/packages/*/*.tgz - key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + key: ${{ env.BUILD_PROFILING_NODE_CACHE_TARBALL_KEY }} - name: Get node version id: versions @@ -1481,7 +1481,7 @@ jobs: - name: Archive Binary # @TODO: v4 breaks convenient merging of same name artifacts # https://github.com/actions/upload-artifact/issues/478 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: profiling-node-binaries-${{ github.sha }} path: | From 73bd57ddad9d64051db9d74148ea62c46ff96fc4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 6 Feb 2024 11:50:27 +0100 Subject: [PATCH 68/68] meta(changelog): Update changelog for v7.100.0 --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7548880d5f7..75a70e414df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,73 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.100.0 + +### Important Changes + +#### Deprecations + +This release includes some deprecations. For more details please look at our +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md). + +The deprecation most likely to affect you is the one of `BrowserTracing`. Instead of `new BrowserTracing()`, you should +now use `browserTracingIntegration()`, which will also handle framework-specific instrumentation out of the box for +you - no need to pass a custom `routingInstrumentation` anymore. For `@sentry/react`, we expose dedicated integrations +for the different react-router versions: + +- `reactRouterV6BrowserTracingIntegration()` +- `reactRouterV5BrowserTracingIntegration()` +- `reactRouterV4BrowserTracingIntegration()` +- `reactRouterV3BrowserTracingIntegration()` + +See the +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md#depreacted-browsertracing-integration) +for details. + +- feat(angular): Export custom `browserTracingIntegration()` (#10353) +- feat(browser): Deprecate `BrowserTracing` integration (#10493) +- feat(browser): Export `browserProfilingIntegration` (#10438) +- feat(bun): Export `bunServerIntegration()` (#10439) +- feat(nextjs): Add `browserTracingIntegration` (#10397) +- feat(react): Add `reactRouterV3BrowserTracingIntegration` for react router v3 (#10489) +- feat(react): Add `reactRouterV4/V5BrowserTracingIntegration` for react router v4 & v5 (#10488) +- feat(react): Add `reactRouterV6BrowserTracingIntegration` for react router v6 & v6.4 (#10491) +- feat(remix): Add custom `browserTracingIntegration` (#10442) +- feat(node): Expose functional integrations to replace classes (#10356) +- feat(vercel-edge): Replace `WinterCGFetch` with `winterCGFetchIntegration` (#10436) +- feat: Deprecate non-callback based `continueTrace` (#10301) +- feat(vue): Deprecate `new VueIntegration()` (#10440) +- feat(vue): Implement vue `browserTracingIntegration()` (#10477) +- feat(sveltekit): Add custom `browserTracingIntegration()` (#10450) + +#### Profiling Node + +`@sentry/profiling-node` has been ported into the monorepo. Future development for it will happen here! + +- pkg(profiling-node): port profiling-node repo to monorepo (#10151) + +### Other Changes + +- feat: Export `setHttpStatus` from all packages (#10475) +- feat(bundles): Add pluggable integrations on CDN to `Sentry` namespace (#10452) +- feat(core): Pass `name` & `attributes` to `tracesSampler` (#10426) +- feat(feedback): Add `system-ui` to start of font family (#10464) +- feat(node-experimental): Add koa integration (#10451) +- feat(node-experimental): Update opentelemetry packages (#10456) +- feat(node-experimental): Update tracing integrations to functional style (#10443) +- feat(replay): Bump `rrweb` to 2.10.0 (#10445) +- feat(replay): Enforce masking of credit card fields (#10472) +- feat(utils): Add `propagationContextFromHeaders` (#10313) +- fix: Make `startSpan`, `startSpanManual` and `startInactiveSpan` pick up the scopes at time of creation instead of + termination (#10492) +- fix(feedback): Fix logo color when colorScheme is "system" (#10465) +- fix(nextjs): Do not report redirects and notFound calls as errors in server actions (#10474) +- fix(nextjs): Fix navigation tracing on app router (#10502) +- fix(nextjs): Apply server action data to correct isolation scope (#10514) +- fix(node): Use normal `require` call to import Undici (#10388) +- ref(nextjs): Remove internally used deprecated APIs (#10453) +- ref(vue): use startInactiveSpan in tracing mixin (#10406) + ## 7.99.0 ### Important Changes