diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba49c52fef94..2ef8d14b6d3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,6 +100,7 @@ jobs: - 'packages/rollup-utils/**' - 'packages/utils/**' - 'packages/types/**' + - 'dev-packages/test-utils/**' browser: &browser - *shared - 'packages/browser/**' @@ -970,22 +971,19 @@ jobs: job_e2e_tests: 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 # 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) + if: always() && needs.job_e2e_prepare.result == 'success' needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 timeout-minutes: 10 env: - E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} - E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} + # We just use a dummy DSN here, only send to the tunnel anyhow + E2E_TEST_DSN: 'https://username@domain/123' # Needed because some apps expect a certain prefix - NEXT_PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} - PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} - REACT_APP_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} + NEXT_PUBLIC_E2E_TEST_DSN: 'https://username@domain/123' + PUBLIC_E2E_TEST_DSN: 'https://username@domain/123' + REACT_APP_E2E_TEST_DSN: 'https://username@domain/123' E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks' E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' strategy: @@ -1014,6 +1012,8 @@ jobs: 'node-express-esm-without-loader', 'node-express-cjs-preload', 'node-otel-sdk-node', + 'ember-classic', + 'ember-embroider', 'nextjs-app-dir', 'nextjs-14', 'nextjs-15', @@ -1069,7 +1069,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 8.3.1 + version: 9.4.0 - name: Set up Node uses: actions/setup-node@v4 with: @@ -1106,12 +1106,12 @@ jobs: - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 5 - run: yarn ${{ matrix.build-command || 'test:build' }} + run: pnpm ${{ matrix.build-command || 'test:build' }} - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 5 - run: yarn test:assert + run: pnpm test:assert - name: Deploy Astro to Cloudflare uses: cloudflare/pages-action@v1 @@ -1146,8 +1146,6 @@ jobs: strategy: fail-fast: false matrix: - is_dependabot: - - ${{ github.actor == 'dependabot[bot]' }} test-application: [ 'react-send-to-sentry', @@ -1156,8 +1154,17 @@ jobs: ] build-command: - false + assert-command: + - false label: - false + include: + - test-application: 'create-remix-app' + assert-command: 'test:assert-sourcemaps' + label: 'create-remix-app (sourcemaps)' + - test-application: 'create-remix-app-legacy' + assert-command: 'test:assert-sourcemaps' + label: 'create-remix-app-legacy (sourcemaps)' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -1166,7 +1173,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 8.3.1 + version: 9.4.0 - name: Set up Node uses: actions/setup-node@v4 with: @@ -1200,12 +1207,12 @@ jobs: - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 5 - run: yarn ${{ matrix.build-command || 'test:build' }} + run: pnpm ${{ matrix.build-command || 'test:build' }} - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 5 - run: yarn test:assert + run: pnpm ${{ matrix.assert-command || 'test:assert' }} job_profiling_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test @@ -1245,7 +1252,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 8.3.1 + version: 9.4.0 - name: Set up Node uses: actions/setup-node@v4 with: diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b8cffb3698ea..9dcbd43e5547 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -56,12 +56,12 @@ jobs: runs-on: ubuntu-20.04 timeout-minutes: 20 env: - E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} - E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} - # Needed because certain apps expect a certain prefix - NEXT_PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} - PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} - REACT_APP_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} + # We just use a dummy DSN here, only send to the tunnel anyhow + E2E_TEST_DSN: 'https://username@domain/123' + # Needed because some apps expect a certain prefix + NEXT_PUBLIC_E2E_TEST_DSN: 'https://username@domain/123' + PUBLIC_E2E_TEST_DSN: 'https://username@domain/123' + REACT_APP_E2E_TEST_DSN: 'https://username@domain/123' E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks' E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' strategy: @@ -103,7 +103,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 8.3.1 + version: 9.4.0 - name: Set up Node uses: actions/setup-node@v4 @@ -151,45 +151,3 @@ jobs: with: filename: .github/CANARY_FAILURE_TEMPLATE.md update_existing: true - - job_ember_canary_test: - name: Ember Canary Tests - runs-on: ubuntu-20.04 - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - # scenario: [ember-release, embroider-optimized, ember-4.0] - scenario: [ember-4.0] - steps: - - name: 'Check out current commit' - 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: Install dependencies - run: yarn install --ignore-engines --frozen-lockfile - - - name: Build dependencies - run: | - yarn lerna run build:types --scope=@sentry/ember --include-dependencies - yarn lerna run build:transpile --scope=@sentry/ember --include-dependencies - - - name: Run Ember tests - run: | - cd packages/ember - yarn ember try:one ${{ matrix.scenario }} --skip-cleanup=true - - - name: Create Issue - if: failure() && github.event_name == 'schedule' - uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RUN_LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - TITLE: Ember Canary ${{ matrix.scenario }} Test Failed - with: - filename: .github/CANARY_FAILURE_TEMPLATE.md - update_existing: true diff --git a/.github/workflows/external-contributors.yml b/.github/workflows/external-contributors.yml index a797d93732eb..294f8fe10af7 100644 --- a/.github/workflows/external-contributors.yml +++ b/.github/workflows/external-contributors.yml @@ -17,8 +17,6 @@ jobs: && github.actor != 'dependabot[bot]' steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - name: Set up Node uses: actions/setup-node@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 469cb8258063..90fb61d73ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.15.0 + +- feat(core): allow unregistering callback through `on` (#11710) +- feat(nestjs): Add function-level span decorator to nestjs (#12721) +- feat(otel): Export & use `spanTimeInputToSeconds` for otel span exporter (#12699) +- fix(core): Pass origin as referrer for `lazyLoadIntegration` (#12766) +- fix(deno): Publish from build directory (#12773) +- fix(hapi): Specify error channel to filter boom errors (#12725) +- fix(react): Revert back to `jsxRuntime: 'classic'` to prevent breaking react 17 (#12775) +- fix(tracing): Report dropped spans for transactions (#12751) +- ref(scope): Delete unused public `getStack()` (#12737) + +Work in this release was contributed by @arturovt and @jaulz. Thank you for your contributions! + ## 8.14.0 ### Important Changes diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 245f21e8d97a..5463bb3eb7a9 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -8,8 +8,10 @@ current state. Prerequisites: Docker - Copy `.env.example` to `.env` -- Fill in auth information in `.env` for an example Sentry project -- Run `yarn build:tarball` in the root of the repository +- OPTIONAL: Fill in auth information in `.env` for an example Sentry project - you only need this to run E2E tests that + send data to Sentry. +- Run `yarn build:tarball` in the root of the repository (needs to be rerun after every update in /packages for the + changes to have effect on the tests). To finally run all of the tests: @@ -90,6 +92,8 @@ test apps enables us to reuse the same test suite over a number of different fra ### Standardized Frontend Test Apps +TODO: This is not up to date. + A standardized frontend test application has the following features: - Just for the sake of consistency we prefix the standardized frontend tests with `standard-frontend-`. For example diff --git a/dev-packages/e2e-tests/lib/validate.ts b/dev-packages/e2e-tests/lib/validate.ts deleted file mode 100644 index 7476067939de..000000000000 --- a/dev-packages/e2e-tests/lib/validate.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable no-console */ - -export function validate(): boolean { - let missingEnvVar = false; - - if (!process.env.E2E_TEST_AUTH_TOKEN) { - console.log( - "No auth token configured! Please configure the E2E_TEST_AUTH_TOKEN environment variable with an auth token that has the scope 'project:read'!", - ); - missingEnvVar = true; - } - - if (!process.env.E2E_TEST_DSN) { - console.log('No DSN configured! Please configure the E2E_TEST_DSN environment variable with a DSN!'); - missingEnvVar = true; - } - - if (!process.env.E2E_TEST_SENTRY_ORG_SLUG) { - console.log( - 'No Sentry organization slug configured! Please configure the E2E_TEST_SENTRY_ORG_SLUG environment variable with a Sentry organization slug!', - ); - missingEnvVar = true; - } - - if (!process.env.E2E_TEST_SENTRY_PROJECT) { - console.log( - 'No Sentry project configured! Please configure the `E2E_TEST_SENTRY_PROJECT` environment variable with a Sentry project slug!', - ); - missingEnvVar = true; - } - - return !missingEnvVar; -} diff --git a/dev-packages/e2e-tests/prepare.ts b/dev-packages/e2e-tests/prepare.ts index 3a230723d9a7..5981d1165164 100644 --- a/dev-packages/e2e-tests/prepare.ts +++ b/dev-packages/e2e-tests/prepare.ts @@ -1,17 +1,12 @@ /* eslint-disable no-console */ import * as dotenv from 'dotenv'; -import { validate } from './lib/validate'; import { registrySetup } from './registrySetup'; async function run(): Promise { // Load environment variables from .env file locally dotenv.config(); - if (!validate()) { - process.exit(1); - } - try { registrySetup(); } catch (error) { diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index c7c2618492cb..f8aafa5eaa01 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -4,9 +4,12 @@ import { resolve } from 'path'; import * as dotenv from 'dotenv'; import { sync as globSync } from 'glob'; -import { validate } from './lib/validate'; import { registrySetup } from './registrySetup'; +const DEFAULT_DSN = 'https://username@domain/123'; +const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; +const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; + function asyncExec(command: string, options: { env: Record; cwd: string }): Promise { return new Promise((resolve, reject) => { const process = spawn(command, { ...options, shell: true }); @@ -39,17 +42,21 @@ async function run(): Promise { // Allow to run a single app only via `yarn test:run ` const appName = process.argv[2]; - if (!validate()) { - process.exit(1); - } + const dsn = process.env.E2E_TEST_DSN || DEFAULT_DSN; const envVarsToInject = { - NEXT_PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN, - PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN, - REACT_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN, + E2E_TEST_DSN: dsn, + NEXT_PUBLIC_E2E_TEST_DSN: dsn, + PUBLIC_E2E_TEST_DSN: dsn, + REACT_APP_E2E_TEST_DSN: dsn, + E2E_TEST_SENTRY_ORG_SLUG: process.env.E2E_TEST_SENTRY_ORG_SLUG || DEFAULT_SENTRY_ORG_SLUG, + E2E_TEST_SENTRY_PROJECT: process.env.E2E_TEST_SENTRY_PROJECT || DEFAULT_SENTRY_PROJECT, }; - const env = { ...process.env, ...envVarsToInject }; + const env = { + ...process.env, + ...envVarsToInject, + }; try { console.log('Cleaning test-applications...'); diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json index 4d41ba051e4b..e16d49c799f4 100644 --- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@sentry-internal/test-utils": "link:../../../test-utils", - "@playwright/test": "^1.41.1", + "@playwright/test": "^1.44.1", "wait-port": "1.0.4" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs index abc7ea7b0ab2..fc4ac82aa7c6 100644 --- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/start-event-proxy.mjs @@ -3,5 +3,4 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, proxyServerName: 'aws-serverless-lambda-layer-cjs', - forwardToSentry: false, }); diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts index 01bd22f3cae0..1d789cb4950c 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/tests/server-transactions.test.ts @@ -1,11 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - test('Sends server-side transactions to Sentry', async ({ baseURL }) => { const transactionEventPromise = waitForTransaction('create-next-app', transactionEvent => { return ( diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx index 5ee250101be9..46a0d015cdc0 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx @@ -21,21 +21,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/_index.tsx index 8c787ebd7c2f..69f39f7bc801 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/_index.tsx @@ -17,8 +17,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/server.mjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/server.mjs index eb6078bf0321..bde5876f4b29 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/server.mjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/server.mjs @@ -48,5 +48,5 @@ app.all( }), ); -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3030; app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`)); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-client.test.ts deleted file mode 100644 index aecc2fa8c983..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-client.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { - await page.goto('/client-error'); - - const exceptionIdHandle = await page.waitForSelector('#event-id'); - const exceptionEventId = await exceptionIdHandle.textContent(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-errors.test.ts new file mode 100644 index 000000000000..2dc323ec4c47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-express-legacy', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-express-legacy', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-transactions.test.ts new file mode 100644 index 000000000000..1dfec26a0a4d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-errors.test.ts new file mode 100644 index 000000000000..8d76e15db32c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-errors.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a loader error to Sentry', async ({ page }) => { + const loaderErrorPromise = waitForError('create-remix-app-express-legacy', errorEvent => { + return errorEvent.exception.values[0].value === 'Loader Error'; + }); + + await page.goto('/loader-error'); + + const loaderError = await loaderErrorPromise; + + expect(loaderError).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts similarity index 77% rename from dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts index cd760d9cc7bc..52cc493d6fe0 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-transactions.test.ts @@ -1,20 +1,9 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; import { uuid4 } from '@sentry/utils'; -test('Sends a loader error to Sentry', async ({ page }) => { - const loaderErrorPromise = waitForError('create-remix-app-express-legacy', errorEvent => { - return errorEvent.exception.values[0].value === 'Loader Error'; - }); - - await page.goto('/loader-error'); - - const loaderError = await loaderErrorPromise; +import { waitForTransaction } from '@sentry-internal/test-utils'; - expect(loaderError).toBeDefined(); -}); - -test('Sends form data with action error to Sentry', async ({ page }) => { +test('Sends form data with action span to Sentry', async ({ page }) => { await page.goto('/action-formdata'); await page.fill('input[name=text]', 'test'); @@ -52,25 +41,16 @@ test('Sends a loader span to Sentry', async ({ page }) => { expect(loaderSpan).toBeDefined(); expect(loaderSpan.op).toBe('function.remix.loader'); }); - test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => { // We use this to identify the transactions const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/client-error?tag=${testTag}`); @@ -104,19 +84,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-express-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx index 4eb7e3d3553f..5a7f045ff5f9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/entry.client.tsx @@ -22,21 +22,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx index b646c62ee4da..40de0390d6ac 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/app/routes/_index.tsx @@ -15,8 +15,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/server.mjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/server.mjs index a3ddf0a15424..710b0265e31c 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/server.mjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/server.mjs @@ -48,5 +48,5 @@ app.all( }), ); -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3030; app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`)); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-client.test.ts deleted file mode 100644 index 6f4f6b17d029..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-client.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-errors.test.ts new file mode 100644 index 000000000000..1838cecb853b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-express-vite-dev', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-express-vite-dev', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts new file mode 100644 index 000000000000..9a5caf46fd66 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts similarity index 79% rename from dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts index f638141dcf57..7faf1e9c684d 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts @@ -8,19 +8,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); @@ -33,7 +25,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id; const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id; - const loaderSpanId = httpServerTransaction.spans.find( + const loaderSpanId = httpServerTransaction?.spans?.find( span => span.data && span.data['code.function'] === 'loader', )?.span_id; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/entry.client.tsx index 5ee250101be9..46a0d015cdc0 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/entry.client.tsx @@ -21,21 +21,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/_index.tsx index 8c787ebd7c2f..69f39f7bc801 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/_index.tsx @@ -17,8 +17,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/server.mjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express/server.mjs index a3ddf0a15424..710b0265e31c 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/server.mjs +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/server.mjs @@ -48,5 +48,5 @@ app.all( }), ); -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3030; app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`)); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-client.test.ts deleted file mode 100644 index aecc2fa8c983..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-client.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { - await page.goto('/client-error'); - - const exceptionIdHandle = await page.waitForSelector('#event-id'); - const exceptionEventId = await exceptionIdHandle.textContent(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-errors.test.ts new file mode 100644 index 000000000000..df815b6d4bb7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-express', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-express', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts new file mode 100644 index 000000000000..a06aa02ceb9c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-errors.test.ts new file mode 100644 index 000000000000..5426bac100b8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-errors.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a loader error to Sentry', async ({ page }) => { + const loaderErrorPromise = waitForError('create-remix-app-express', errorEvent => { + return errorEvent.exception.values[0].value === 'Loader Error'; + }); + + await page.goto('/loader-error'); + + const loaderError = await loaderErrorPromise; + + expect(loaderError).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts similarity index 79% rename from dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts index 292d827c783e..79b10331d18e 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts @@ -1,20 +1,9 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; import { uuid4 } from '@sentry/utils'; -test('Sends a loader error to Sentry', async ({ page }) => { - const loaderErrorPromise = waitForError('create-remix-app-express', errorEvent => { - return errorEvent.exception.values[0].value === 'Loader Error'; - }); - - await page.goto('/loader-error'); - - const loaderError = await loaderErrorPromise; - - expect(loaderError).toBeDefined(); -}); +import { waitForTransaction } from '@sentry-internal/test-utils'; -test('Sends form data with action error to Sentry', async ({ page }) => { +test('Sends form data with action span', async ({ page }) => { await page.goto('/action-formdata'); await page.fill('input[name=text]', 'test'); @@ -62,19 +51,11 @@ test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => { const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/client-error?tag=${testTag}`); @@ -111,19 +92,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/entry.client.tsx index 93eab0f819fb..d0c95287e0c9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/entry.client.tsx @@ -21,21 +21,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/routes/_index.tsx index b646c62ee4da..40de0390d6ac 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/app/routes/_index.tsx @@ -15,8 +15,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json index d70c8f824dbc..4b7c2c162b86 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/package.json @@ -2,13 +2,15 @@ "private": true, "sideEffects": false, "scripts": { - "build": "remix build --sourcemap && ./upload-sourcemaps.sh", + "build": "remix build --sourcemap", + "upload-sourcemaps": "./upload-sourcemaps.sh", "dev": "remix dev", "start": "remix-serve build", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && npx playwright install && pnpm build", - "test:assert": "pnpm playwright test" + "test:assert": "pnpm playwright test", + "test:assert-sourcemaps": "pnpm upload-sourcemaps" }, "dependencies": { "@sentry/remix": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-client.test.ts deleted file mode 100644 index aecc2fa8c983..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-client.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { - await page.goto('/client-error'); - - const exceptionIdHandle = await page.waitForSelector('#event-id'); - const exceptionEventId = await exceptionIdHandle.textContent(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-errors.test.ts new file mode 100644 index 000000000000..c538b2fdf09a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-legacy', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-legacy', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-transactions.test.ts new file mode 100644 index 000000000000..e6d2b3d33358 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-legacy', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-legacy', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/server-transactions.test.ts similarity index 80% rename from dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/server-transactions.test.ts index 7354257a2dd0..99742260c04f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/server-transactions.test.ts @@ -8,19 +8,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/entry.client.tsx index b3b5db3d9b3d..2109aad0a421 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/entry.client.tsx @@ -28,21 +28,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/routes/_index.tsx index b646c62ee4da..40de0390d6ac 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/app/routes/_index.tsx @@ -15,8 +15,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-client.test.ts deleted file mode 100644 index aecc2fa8c983..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-client.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { - await page.goto('/client-error'); - - const exceptionIdHandle = await page.waitForSelector('#event-id'); - const exceptionEventId = await exceptionIdHandle.textContent(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-errors.test.ts new file mode 100644 index 000000000000..1e7aa050756e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-v2-legacy', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-v2-legacy', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-transactions.test.ts new file mode 100644 index 000000000000..6c6a23405b4c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-v2-legacy', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-v2-legacy', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/server-transactions.test.ts similarity index 80% rename from dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/server-transactions.test.ts index d19dec15e0bc..b2e7ac23f495 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/server-transactions.test.ts @@ -8,19 +8,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-v2-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-v2-legacy', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx index b3b5db3d9b3d..2109aad0a421 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx @@ -28,21 +28,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx index b646c62ee4da..40de0390d6ac 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/app/routes/_index.tsx @@ -15,8 +15,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts deleted file mode 100644 index aecc2fa8c983..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { - await page.goto('/client-error'); - - const exceptionIdHandle = await page.waitForSelector('#event-id'); - const exceptionEventId = await exceptionIdHandle.textContent(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-errors.test.ts new file mode 100644 index 000000000000..bcee1941e9f8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-v2', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app-v2', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts new file mode 100644 index 000000000000..39fd065aa959 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts similarity index 81% rename from dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts index ea95b97fa611..01facd7cc263 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts @@ -8,19 +8,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app-v2', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx index 93eab0f819fb..d0c95287e0c9 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx @@ -21,21 +21,6 @@ Sentry.init({ tunnel: 'http://localhost:3031/', // proxy server }); -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - startTransition(() => { hydrateRoot( document, diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx index b646c62ee4da..40de0390d6ac 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx @@ -15,8 +15,7 @@ export default function Index() { value="Capture Exception" id="exception-button" onClick={() => { - const eventId = Sentry.captureException(new Error('I am an error!')); - window.capturedExceptionId = eventId; + throw new Error('I am an error!'); }} /> diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json index 1db0d3858918..db5c5b474ef0 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/package.json @@ -2,13 +2,15 @@ "private": true, "sideEffects": false, "scripts": { - "build": "remix build --sourcemap && ./upload-sourcemaps.sh", + "build": "remix build --sourcemap", + "upload-sourcemaps": "./upload-sourcemaps.sh", "dev": "remix dev", "start": "NODE_OPTIONS='--require=./instrument.server.cjs' remix-serve build", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && npx playwright install && pnpm build", - "test:assert": "pnpm playwright test" + "test:assert": "pnpm playwright test", + "test:assert-sourcemaps": "pnpm upload-sourcemaps" }, "dependencies": { "@sentry/remix": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts deleted file mode 100644 index aecc2fa8c983..000000000000 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { expect, test } from '@playwright/test'; - -const EVENT_POLLING_TIMEOUT = 90_000; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; - -test('Sends a client-side exception to Sentry', async ({ page }) => { - await page.goto('/'); - - const exceptionButton = page.locator('id=exception-button'); - await exceptionButton.click(); - - const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); - const exceptionEventId = await exceptionIdHandle.jsonValue(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Sends a pageload transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageLoadTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'pageload') { - hadPageLoadTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageLoadTransaction).toBe(true); -}); - -test('Sends a navigation transaction to Sentry', async ({ page }) => { - await page.goto('/'); - - // Give pageload transaction time to finish - await page.waitForTimeout(4000); - - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const recordedTransactionsHandle = await page.waitForFunction(() => { - if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { - return window.recordedTransactions; - } else { - return undefined; - } - }); - const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); - - if (recordedTransactionEventIds === undefined) { - throw new Error("Application didn't record any transaction event IDs."); - } - - let hadPageNavigationTransaction = false; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); - - await Promise.all( - recordedTransactionEventIds.map(async transactionEventId => { - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - if (response.ok) { - const data = await response.json(); - if (data.contexts.trace.op === 'navigation') { - hadPageNavigationTransaction = true; - } - } - - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); - }), - ); - - expect(hadPageNavigationTransaction).toBe(true); -}); - -test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { - await page.goto('/client-error'); - - const exceptionIdHandle = await page.waitForSelector('#event-id'); - const exceptionEventId = await exceptionIdHandle.textContent(); - - console.log(`Polling for error eventId: ${exceptionEventId}`); - - await expect - .poll( - async () => { - const response = await fetch( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - return response.status; - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { - await page.goto('/'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); - -test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { - await page.goto('/user/123'); - - const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { - state: 'attached', - }); - const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { - state: 'attached', - }); - - expect(sentryTraceMetaTag).toBeTruthy(); - expect(baggageMetaTag).toBeTruthy(); -}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-errors.test.ts new file mode 100644 index 000000000000..e28464dd3c1b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); + +test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { + const errorPromise = waitForError('create-remix-app', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'Sentry React Component Error'; + }); + + await page.goto('/client-error'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-transactions.test.ts new file mode 100644 index 000000000000..0dc748d2d85d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-transactions.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + const transactionPromise = waitForTransaction('create-remix-app', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id'; + }); + + await page.goto('/'); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionPromise; + + expect(transactionEvent).toBeDefined(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); + +test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => { + await page.goto('/user/123'); + + const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', { + state: 'attached', + }); + const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', { + state: 'attached', + }); + + expect(sentryTraceMetaTag).toBeTruthy(); + expect(baggageMetaTag).toBeTruthy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/server-transactions.test.ts similarity index 81% rename from dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts rename to dev-packages/e2e-tests/test-applications/create-remix-app/tests/server-transactions.test.ts index 45f24ad9d18b..e9e729ba6dc7 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-server.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app/tests/server-transactions.test.ts @@ -8,19 +8,11 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const testTag = uuid4(); const httpServerTransactionPromise = waitForTransaction('create-remix-app', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'http.server' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; }); const pageLoadTransactionPromise = waitForTransaction('create-remix-app', transactionEvent => { - return ( - transactionEvent.type === 'transaction' && - transactionEvent.contexts?.trace?.op === 'pageload' && - transactionEvent.tags?.['sentry_test'] === testTag - ); + return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; }); page.goto(`/?tag=${testTag}`); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/.editorconfig b/dev-packages/e2e-tests/test-applications/ember-classic/.editorconfig new file mode 100644 index 000000000000..c35a002406b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/.ember-cli b/dev-packages/e2e-tests/test-applications/ember-classic/.ember-cli new file mode 100644 index 000000000000..4ccb4bf43700 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/.ember-cli @@ -0,0 +1,15 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": false, + + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/.gitignore b/dev-packages/e2e-tests/test-applications/ember-classic/.gitignore new file mode 100644 index 000000000000..f1e859b291c4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/.gitignore @@ -0,0 +1,32 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/.env* +/.pnp* +/.sass-cache +/.eslintcache +/connect.lock +/coverage/ +/libpeerconnection.log +/npm-debug.log* +/testem.log +/yarn-error.log + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/npm-shrinkwrap.json.ember-try +/package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try + +# broccoli-debug +/DEBUG/ diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/.npmrc b/dev-packages/e2e-tests/test-applications/ember-classic/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/.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/ember-classic/.watchmanconfig b/dev-packages/e2e-tests/test-applications/ember-classic/.watchmanconfig new file mode 100644 index 000000000000..e7834e3e4f39 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/README.md b/dev-packages/e2e-tests/test-applications/ember-classic/README.md new file mode 100644 index 000000000000..e07f12913b1d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/README.md @@ -0,0 +1,56 @@ +# ember-classic + +This README outlines the details of collaborating on this Ember application. A short introduction of this app could +easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) (with npm) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd ember-classic` +- `npm install` + +## Running / Development + +- `ember serve` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `ember test` +- `ember test --server` + +### Linting + +- `npm run lint` +- `npm run lint:fix` + +### Building + +- `ember build` (development) +- `ember build --environment production` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts new file mode 100644 index 000000000000..a37eadb8fff6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts @@ -0,0 +1,20 @@ +import Application from '@ember/application'; +import * as Sentry from '@sentry/ember'; +import loadInitializers from 'ember-load-initializers'; +import Resolver from 'ember-resolver'; + +import config from './config/environment'; + +Sentry.init({ + replaysSessionSampleRate: 1, + replaysOnErrorSampleRate: 1, + tunnel: `http://localhost:3031/`, // proxy server +}); + +export default class App extends Application { + public modulePrefix = config.modulePrefix; + public podModulePrefix = config.podModulePrefix; + public Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.hbs new file mode 100644 index 000000000000..c6a18f9e37cc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.hbs @@ -0,0 +1,3 @@ + + {{yield}} + diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.ts new file mode 100644 index 000000000000..1ba66df216fc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.ts @@ -0,0 +1,33 @@ +import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; + +interface Args { + route: string; +} + +/* + Note: We use this custom component instead of the built-in ``, + as that is an ember component in older versions, and a glimmer component in newer versions. + + Since glimmer components are, as of now, not instrumented, this leads to different test results. +*/ +export default class LinkComponent extends Component { + @service public declare router: RouterService; + + public get href(): string { + return this.router.urlFor(this.args.route); + } + + public get isActive(): boolean { + return this.router.currentRouteName === this.args.route; + } + + @action + public onClick(event: MouseEvent): void { + event.preventDefault(); + + void this.router.transitionTo(this.args.route); + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-gc-list.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-gc-list.ts new file mode 100644 index 000000000000..3ac89dc43ca7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-gc-list.ts @@ -0,0 +1,4 @@ +/* eslint-disable ember/no-empty-glimmer-component-classes */ +import Component from '@glimmer/component'; + +export default class SlowLoadingGCList extends Component {} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-list.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-list.ts new file mode 100644 index 000000000000..e766fe78609f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-list.ts @@ -0,0 +1,25 @@ +/* eslint-disable ember/no-classic-classes */ +/* eslint-disable ember/no-classic-components */ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +interface Args { + title?: string; + items: number; +} + +export default Component.extend({ + tagName: '', + + _title: computed('title', function () { + return (this as Args).title || 'Slow Loading List'; + }), + + rowItems: computed('items', function () { + return new Array((this as Args).items).fill(0).map((_, index) => { + return { + index: index + 1, + }; + }); + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/components/test-section.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/test-section.ts new file mode 100644 index 000000000000..d0ca7e8edabc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/components/test-section.ts @@ -0,0 +1,6 @@ +/* eslint-disable ember/no-classic-classes */ +/* eslint-disable ember/no-classic-components */ +/* eslint-disable ember/require-tagless-components */ +import Component from '@ember/component'; + +export default Component.extend({}); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/config/environment.d.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/config/environment.d.ts new file mode 100644 index 000000000000..8a8a687909e4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/config/environment.d.ts @@ -0,0 +1,17 @@ +/** + * Type declarations for + * import config from './config/environment' + * + * For now these need to be managed by the developer + * since different ember addons can materialize new entries. + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none' | 'auto'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts new file mode 100644 index 000000000000..c49ff8d94147 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts @@ -0,0 +1,63 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { scheduleOnce } from '@ember/runloop'; +import { tracked } from '@glimmer/tracking'; +import { Promise } from 'rsvp'; + +export default class IndexController extends Controller { + @tracked public showComponents = false; + + @action + public createError(): void { + // @ts-expect-error this is fine + this.nonExistentFunction(); + } + + @action + public createEmberError(): void { + throw new Error('Whoops, looks like you have an EmberError'); + } + + @action + public createCaughtEmberError(): void { + try { + throw new Error('Looks like you have a caught EmberError'); + } catch (e) { + // do nothing + } + } + + @action + public createFetchError(): void { + void fetch('http://doesntexist.example'); + } + + @action + public createAfterRenderError(): void { + function throwAfterRender(): void { + throw new Error('After Render Error'); + } + scheduleOnce('afterRender', null, throwAfterRender); + } + + @action + public createRSVPRejection(): Promise { + const promise = new Promise((resolve, reject) => { + reject('Promise rejected'); + }); + return promise; + } + + @action + public createRSVPError(): Promise { + const promise = new Promise(() => { + throw new Error('Error within RSVP Promise'); + }); + return promise; + } + + @action + public toggleShowComponents(): void { + this.showComponents = !this.showComponents; + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route.ts new file mode 100644 index 000000000000..01a523ea0985 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route.ts @@ -0,0 +1,13 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; + +export default class SlowLoadingRouteController extends Controller { + @service public declare router: RouterService; + + @action + public back(): void { + void this.router.transitionTo('tracing'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route/index.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route/index.ts new file mode 100644 index 000000000000..b66350b5c911 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route/index.ts @@ -0,0 +1,5 @@ +import Controller from '@ember/controller'; + +export default class SlowLoadingRouteController extends Controller { + public slowLoadingTemplateOnlyItems = new Array(2000).fill(0).map((_, index) => index); +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/tracing.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/tracing.ts new file mode 100644 index 000000000000..72c0d635702e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/tracing.ts @@ -0,0 +1,13 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; + +export default class TracingController extends Controller { + @service public declare router: RouterService; + + @action + public navigateToSlowRoute(): void { + void this.router.transitionTo('slow-loading-route'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/helpers/utils.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/helpers/utils.ts new file mode 100644 index 000000000000..60a3f2956224 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/helpers/utils.ts @@ -0,0 +1,3 @@ +export default function timeout(time: number): Promise { + return new Promise(resolve => setTimeout(resolve, time)); +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/index.html b/dev-packages/e2e-tests/test-applications/ember-classic/app/index.html new file mode 100644 index 000000000000..4be4ec8973e5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/index.html @@ -0,0 +1,24 @@ + + + + + EmberClassic + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/initializers/deprecation.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/initializers/deprecation.ts new file mode 100644 index 000000000000..fcc2d180532e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/initializers/deprecation.ts @@ -0,0 +1,13 @@ +import { registerDeprecationHandler } from '@ember/debug'; + +export function initialize(): void { + registerDeprecationHandler((message, options, next) => { + if (options && options.until && options.until !== '3.0.0') { + return; + } else { + next(message, options); + } + }); +} + +export default { initialize }; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/router.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/router.ts new file mode 100644 index 000000000000..e13dec6d82c5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/router.ts @@ -0,0 +1,18 @@ +import EmberRouter from '@ember/routing/router'; + +import config from './config/environment'; + +export default class Router extends EmberRouter { + public location = config.locationType; + public rootURL = config.rootURL; +} + +// This is a false positive of the eslint rule +// eslint-disable-next-line array-callback-return +Router.map(function () { + this.route('tracing'); + this.route('replay'); + this.route('slow-loading-route', function () { + this.route('index', { path: '/' }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/replay.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/replay.ts new file mode 100644 index 000000000000..20e5200760b3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/replay.ts @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; +import type { BrowserClient } from '@sentry/ember'; +import * as Sentry from '@sentry/ember'; + +export default class ReplayRoute extends Route { + public async beforeModel(): Promise { + const { replayIntegration } = Sentry; + const client = Sentry.getClient(); + if (client && !client.getIntegrationByName('Replay')) { + client.addIntegration(replayIntegration()); + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route.ts new file mode 100644 index 000000000000..96f57bd9cf2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route.ts @@ -0,0 +1,26 @@ +import Route from '@ember/routing/route'; +import { instrumentRoutePerformance } from '@sentry/ember'; + +import timeout from '../helpers/utils'; + +const SLOW_TRANSITION_WAIT = 1500; + +class SlowDefaultLoadingRoute extends Route { + public beforeModel(): Promise { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + public model(): Promise { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + public afterModel(): Promise { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + public setupController(...rest: Parameters): ReturnType { + super.setupController(...rest); + } +} + +export default instrumentRoutePerformance(SlowDefaultLoadingRoute); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route/index.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route/index.ts new file mode 100644 index 000000000000..c810ca5e2505 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route/index.ts @@ -0,0 +1,26 @@ +import Route from '@ember/routing/route'; +import { instrumentRoutePerformance } from '@sentry/ember'; + +import timeout from '../../helpers/utils'; + +const SLOW_TRANSITION_WAIT = 1500; + +class SlowLoadingRoute extends Route { + public beforeModel(): Promise { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + public model(): Promise { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + public afterModel(): Promise { + return timeout(SLOW_TRANSITION_WAIT / 3); + } + + public setupController(...rest: Parameters): ReturnType { + super.setupController(...rest); + } +} + +export default instrumentRoutePerformance(SlowLoadingRoute); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/styles/app.css b/dev-packages/e2e-tests/test-applications/ember-classic/app/styles/app.css new file mode 100644 index 000000000000..f926764e8b3d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/styles/app.css @@ -0,0 +1,197 @@ +:root { + --primary-fg-color: #6c5fc7; + --button-border-color: #413496; + --foreground-color: #2f2936; + --background-color: #f2f1f3; + --content-border-color: #e2dee6; + --button-background-hover-color: #5b4cc0; +} + +html { + height: 100vh; +} + +body { + background: var(--background-color) url('/assets/images/sentry-pattern-transparent.png'); + background-size: 340px; + background-repeat: repeat; + height: 100%; + margin: 0; + font-family: + Rubik, + Avenir Next, + Helvetica Neue, + sans-serif; + font-size: 16px; + line-height: 24px; + color: var(--foreground-color); +} + +.app { + display: flex; + flex-direction: column; + flex-grow: 1; + align-items: center; +} + +.container { + position: relative; + padding-left: 30px; + padding-right: 30px; + padding-top: 5vh; + width: 100%; + max-width: 740px; + flex: 1; +} + +.box { + background-color: #fff; + border: 0; + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.08), + 0 1px 4px rgba(0, 0, 0, 0.1); + border-radius: 4px; + display: flex; + width: 100%; + margin: 0 0 20px; +} + +.sidebar { + padding-top: 20px; + width: 60px; + background: #564f64; + background-image: linear-gradient(-180deg, rgba(52, 44, 62, 0), rgba(52, 44, 62, 0.5)); + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.1); + border-radius: 4px 0 0 4px; + margin-top: -1px; + margin-bottom: -1px; + text-align: center; + + display: flex; + justify-content: center; + padding-top: 20px; + padding-bottom: 20px; +} + +.logo { + width: 24px; + height: 24px; + background-image: url('/assets/images/sentry-logo.svg'); +} + +.nav { + display: flex; + justify-content: center; + padding: 10px; + padding-top: 20px; + padding-bottom: 0px; +} + +.nav a { + padding-left: 10px; + padding-right: 10px; + font-weight: 500; + text-decoration: none; + color: var(--foreground-color); +} + +.nav a.active { + border-bottom: 4px solid #6c5fc7; +} + +section.content { + flex: 1; + padding-bottom: 40px; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 600; +} + +h3 { + font-size: 24px; + line-height: 1.2; +} + +div.section { + margin-top: 20px; +} + +.section h4 { + margin-bottom: 10px; +} + +.content-container { + padding-left: 40px; + padding-right: 40px; + padding-top: 20px; +} + +.content-container h3, +.content-container h4 { + margin-top: 0px; +} + +.border-bottom { + border-bottom: 1px solid var(--content-border-color); +} + +button { + border-radius: 3px; + font-weight: 600; + padding: 8px 16px; + transition: all 0.1s; + + border: 1px solid transparent; + border-radius: 3px; + font-weight: 600; + padding: 8px 16px; + + -webkit-appearance: button; + cursor: pointer; +} + +button:hover { + text-decoration: none; +} + +button:focus { + outline-offset: -2px; +} + +button.primary { + color: #fff; + background-color: var(--primary-fg-color); + border-color: var(--button-border-color); + + display: inline-block; + + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.08); + + text-transform: none; + overflow: visible; +} + +button.primary:hover { + background-color: var(--button-background-hover-color); + border-color: #204d74; +} + +button.primary:focus { + background: #5b4cc0; + border-color: #3a2f87; + box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.12); + outline: none; +} + +.list-grid { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 10px; +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/application.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/application.hbs new file mode 100644 index 000000000000..09e6d2fffbb3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/application.hbs @@ -0,0 +1,22 @@ +
+
+
+ +
+
+

Sentry Instrumented Ember Application

+
+ +
+ {{outlet}} +
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-gc-list.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-gc-list.hbs new file mode 100644 index 000000000000..800b0344a1c3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-gc-list.hbs @@ -0,0 +1,10 @@ +
+

{{@title}}

+
+ {{#each @rowItems as |rowItem|}} +
+ {{rowItem}} +
+ {{/each}} +
+
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-list.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-list.hbs new file mode 100644 index 000000000000..88e1a0db9371 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-list.hbs @@ -0,0 +1,10 @@ +
+

{{this._title}}

+
+ {{#each this.rowItems as |rowItem|}} +
+ {{rowItem.index}} +
+ {{/each}} +
+
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/test-section.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/test-section.hbs new file mode 100644 index 000000000000..9204d2d7571b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/test-section.hbs @@ -0,0 +1,6 @@ +
+

{{@title}}

+ +
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/index.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/index.hbs new file mode 100644 index 000000000000..b39ffce5c0e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/index.hbs @@ -0,0 +1,11 @@ + + + + + + + +{{outlet}} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/replay.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/replay.hbs new file mode 100644 index 000000000000..effb884d8f5f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/replay.hbs @@ -0,0 +1 @@ +

Visiting this page starts Replay!

diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route.hbs new file mode 100644 index 000000000000..72981cae67d7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route.hbs @@ -0,0 +1,11 @@ +

Intentionally Slow Route

+ + +
+ +
+ {{outlet}} +
+
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route/index.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route/index.hbs new file mode 100644 index 000000000000..45de2bc44207 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route/index.hbs @@ -0,0 +1,5 @@ +
+ + +
diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/tracing.hbs b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/tracing.hbs new file mode 100644 index 000000000000..b2e5087f9f3e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/templates/tracing.hbs @@ -0,0 +1,2 @@ + diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/config/ember-cli-update.json b/dev-packages/e2e-tests/test-applications/ember-classic/config/ember-cli-update.json new file mode 100644 index 000000000000..2ace44409e0b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/config/ember-cli-update.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "ember-cli", + "version": "4.8.0", + "blueprints": [ + { + "name": "app", + "outputRepo": "https://github.com/ember-cli/ember-new-output", + "codemodsSource": "ember-app-codemods-manifest@1", + "isBaseBlueprint": true, + "options": ["--no-welcome", "--ci-provider=github"] + } + ] + } + ] +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/config/environment.js b/dev-packages/e2e-tests/test-applications/ember-classic/config/environment.js new file mode 100644 index 000000000000..54919f9d6c9d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/config/environment.js @@ -0,0 +1,64 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-classic', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + ENV['@sentry/ember'] = { + sentry: { + tracesSampleRate: 1, + dsn: process.env.E2E_TEST_DSN, + tracePropagationTargets: ['localhost', 'doesntexist.example'], + browserTracingOptions: { + _experiments: { + // This lead to some flaky tests, as that is sometimes logged + enableLongTask: false, + }, + }, + }, + ignoreEmberOnErrorWarning: true, + minimumRunloopQueueDuration: 0, + minimumComponentRenderDuration: 0, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/config/optional-features.json b/dev-packages/e2e-tests/test-applications/ember-classic/config/optional-features.json new file mode 100644 index 000000000000..b26286e2ecdf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/config/optional-features.json @@ -0,0 +1,6 @@ +{ + "application-template-wrapper": false, + "default-async-observers": true, + "jquery-integration": false, + "template-only-glimmer-components": true +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/config/targets.js b/dev-packages/e2e-tests/test-applications/ember-classic/config/targets.js new file mode 100644 index 000000000000..9f6cc639666e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/config/targets.js @@ -0,0 +1,7 @@ +'use strict'; + +const browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; + +module.exports = { + browsers, +}; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/ember-cli-build.js b/dev-packages/e2e-tests/test-applications/ember-classic/ember-cli-build.js new file mode 100644 index 000000000000..6d9689fa1bc0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/ember-cli-build.js @@ -0,0 +1,24 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + tests: false, + }); + + // Use `app.import` to add additional libraries to the generated + // output files. + // + // If you need to use different assets in different + // environments, specify an object as the first parameter. That + // object's keys should be the environment name and the values + // should be the asset to use in that environment. + // + // If the library that you are including contains AMD or ES6 + // modules that you would like to import into your application + // please specify an object with the list of modules as keys + // along with the exports of each module as its value. + + return app.toTree(); +}; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/package.json b/dev-packages/e2e-tests/test-applications/ember-classic/package.json new file mode 100644 index 000000000000..2de3891d90af --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json @@ -0,0 +1,83 @@ +{ + "name": "ember-classic", + "version": "0.0.0", + "private": true, + "description": "Small description for ember-classic goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "proxy": "ts-node-script start-event-proxy.ts", + "build": "ember build --environment=production", + "start": "ember serve --prod", + "test": "playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add ember-source@latest && npx playwright install && pnpm build", + "test:assert": "playwright test", + "clean": "npx rimraf node_modules,pnpm-lock.yaml,dist" + }, + "devDependencies": { + "@ember/optional-features": "^2.0.0", + "@glimmer/component": "^1.1.2", + "@glimmer/tracking": "^1.1.2", + "@playwright/test": "^1.44.1", + "@ember/string": "^3.1.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/ember": "latest || *", + "@tsconfig/ember": "^3.0.6", + "@tsconfig/node18": "18.2.4", + "@types/ember": "^4.0.11", + "@types/ember-resolver": "^9.0.0", + "@types/ember__application": "^4.0.11", + "@types/ember__array": "^4.0.10", + "@types/ember__component": "^4.0.22", + "@types/ember__controller": "^4.0.12", + "@types/ember__debug": "^4.0.8", + "@types/ember__destroyable": "^4.0.5", + "@types/ember__engine": "^4.0.11", + "@types/ember__error": "^4.0.6", + "@types/ember__object": "^4.0.12", + "@types/ember__polyfills": "^4.0.6", + "@types/ember__routing": "^4.0.22", + "@types/ember__runloop": "^4.0.10", + "@types/ember__service": "^4.0.9", + "@types/ember__string": "^3.0.15", + "@types/ember__template": "^4.0.7", + "@types/ember__utils": "^4.0.7", + "@types/node": "18.18.0", + "@types/rsvp": "^4.0.9", + "broccoli-asset-rev": "^3.0.0", + "ember-auto-import": "^2.4.3", + "ember-cli": "~4.8.0", + "ember-cli-app-version": "^5.0.0", + "ember-cli-babel": "^7.26.11", + "ember-cli-dependency-checker": "^3.3.1", + "ember-cli-htmlbars": "^6.1.1", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-sri": "^2.1.1", + "ember-cli-terser": "^4.0.2", + "ember-cli-typescript": "^5.3.0", + "ember-fetch": "^8.1.2", + "ember-load-initializers": "^2.1.2", + "ember-page-title": "^7.0.0", + "ember-qunit": "^6.0.0", + "ember-resolver": "^8.0.3", + "ember-source": "~4.8.0", + "loader.js": "^4.7.0", + "ts-node": "10.9.1", + "typescript": "^5.4.5" + }, + "engines": { + "node": "14.* || 16.* || >= 18" + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts b/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts new file mode 100644 index 000000000000..6c2442587de4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts @@ -0,0 +1,73 @@ +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 emberPort = 4020; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30_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, + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${emberPort}`, + + /* 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: `pnpm ember serve --path=dist/ --port=${emberPort}`, + port: emberPort, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-logo.svg b/dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-logo.svg new file mode 100644 index 000000000000..bac4e57b7790 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-logo.svg @@ -0,0 +1 @@ +logos diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-pattern-transparent.png b/dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-pattern-transparent.png new file mode 100644 index 000000000000..1f7312b5f6af Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-pattern-transparent.png differ diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/public/robots.txt b/dev-packages/e2e-tests/test-applications/ember-classic/public/robots.txt new file mode 100644 index 000000000000..f5916452e5ff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/public/robots.txt @@ -0,0 +1,3 @@ +# http://www.robotstxt.org +User-agent: * +Disallow: diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/ember-classic/start-event-proxy.ts new file mode 100644 index 000000000000..4d7a6a8c0320 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'ember-classic', +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts new file mode 100644 index 000000000000..2e836cf8b756 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('ember-classic', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + + await page.locator('[data-test-button="Throw Generic Javascript Error"]').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'TypeError', + value: 'this.nonExistentFunction is not a function', + mechanism: { + type: 'instrument', + handled: false, + }, + }, + ], + }, + transaction: 'route:index', + }); +}); + +test('assigns the correct transaction value after a navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorPromise = waitForError('ember-classic', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/tracing`); + await pageloadTxnPromise; + + await page.getByText('Errors').click(); + + const [_, error] = await Promise.all([ + page.locator('[data-test-button="Throw Generic Javascript Error"]').click(), + errorPromise, + ]); + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'TypeError', + value: 'this.nonExistentFunction is not a function', + mechanism: { + type: 'instrument', + handled: false, + }, + }, + ], + }, + transaction: 'route:index', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts new file mode 100644 index 000000000000..47f3cdb0a6b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts @@ -0,0 +1,279 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('ember-classic', 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.ember', + }, + }, + transaction: 'route:index', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + const [_, navigationTxn] = await Promise.all([page.getByText('Tracing').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.ember', + }, + }, + transaction: 'route:tracing', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, pageloadTxn, navigationTxn] = await Promise.all([ + page.getByText('Tracing').click(), + pageloadTxnPromise, + navigationTxnPromise, + ]); + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.ember', + }, + }, + transaction: 'route:index', + transaction_info: { + source: 'route', + }, + }); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.ember', + }, + }, + transaction: 'route:tracing', + transaction_info: { + source: 'route', + }, + }); +}); + +test('captures correct spans for navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/tracing`); + await pageloadTxnPromise; + + const [_, navigationTxn] = await Promise.all([page.getByText('Measure Things!').click(), navigationTxnPromise]); + + const traceId = navigationTxn.contexts?.trace?.trace_id; + const spanId = navigationTxn.contexts?.trace?.span_id; + + expect(traceId).toBeDefined(); + expect(spanId).toBeDefined(); + + const spans = navigationTxn.spans || []; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.ember', + }, + }, + transaction: 'route:slow-loading-route.index', + transaction_info: { + source: 'route', + }, + }); + + const transitionSpans = spans.filter(span => span.op === 'ui.ember.transition'); + const beforeModelSpans = spans.filter(span => span.op === 'ui.ember.route.before_model'); + const modelSpans = spans.filter(span => span.op === 'ui.ember.route.model'); + const afterModelSpans = spans.filter(span => span.op === 'ui.ember.route.after_model'); + const renderSpans = spans.filter(span => span.op === 'ui.ember.runloop.render'); + + expect(transitionSpans).toHaveLength(1); + + // We have two spans each there - one for `slow-loading-route` and one for `slow-load-route.index` + expect(beforeModelSpans).toHaveLength(2); + expect(modelSpans).toHaveLength(2); + expect(afterModelSpans).toHaveLength(2); + + // There may be many render spans... + expect(renderSpans.length).toBeGreaterThan(1); + + expect(transitionSpans[0]).toEqual({ + data: { + 'sentry.op': 'ui.ember.transition', + 'sentry.origin': 'auto.ui.ember', + }, + description: 'route:tracing -> route:slow-loading-route.index', + op: 'ui.ember.transition', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + expect(beforeModelSpans).toEqual([ + { + data: { + 'sentry.op': 'ui.ember.route.before_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route', + op: 'ui.ember.route.before_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.op': 'ui.ember.route.before_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route.index', + op: 'ui.ember.route.before_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + + expect(modelSpans).toEqual([ + { + data: { + 'sentry.op': 'ui.ember.route.model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route', + op: 'ui.ember.route.model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.op': 'ui.ember.route.model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route.index', + op: 'ui.ember.route.model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + + expect(afterModelSpans).toEqual([ + { + data: { + 'sentry.op': 'ui.ember.route.after_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route', + op: 'ui.ember.route.after_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.op': 'ui.ember.route.after_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route.index', + op: 'ui.ember.route.after_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + + expect(renderSpans).toContainEqual({ + data: { + 'sentry.op': 'ui.ember.runloop.render', + 'sentry.origin': 'auto.ui.ember', + }, + description: 'runloop', + op: 'ui.ember.runloop.render', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json new file mode 100644 index 000000000000..877f7b3990f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "extends": "@tsconfig/ember/tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "noEmit": true, + + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "ember-classic/*": [ + "app/*" + ], + "*": [ + "types/*" + ], + } + }, + "include": [ + "app/**/*", + "types/**/*" + ], + "exclude": ["tests/**/*"], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } + +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.json b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.json new file mode 100644 index 000000000000..78f134a16dca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/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/ember-classic/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.node.json new file mode 100644 index 000000000000..65950d5c2bf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json", + "include": ["playwright.config.*", "start-event-proxy.ts"], + "compilerOptions": { + "composite": true, + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Node", + "types": ["node"] + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/types/ember-classic/index.d.ts b/dev-packages/e2e-tests/test-applications/ember-classic/types/ember-classic/index.d.ts new file mode 100644 index 000000000000..d2f5fc1b01a7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/types/ember-classic/index.d.ts @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +declare global { + // Prevents ESLint from "fixing" this via its auto-fix to turn it into a type + // alias (e.g. after running any Ember CLI generator) + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Array extends Ember.ArrayPrototypeExtensions {} + // interface Function extends Ember.FunctionPrototypeExtensions {} +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/types/global.d.ts b/dev-packages/e2e-tests/test-applications/ember-classic/types/global.d.ts new file mode 100644 index 000000000000..a8988b72222e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-classic/types/global.d.ts @@ -0,0 +1,7 @@ +// Types for compiled templates +declare module 'ember-classic/templates/*' { + import { TemplateFactory } from 'ember-cli-htmlbars'; + + const tmpl: TemplateFactory; + export default tmpl; +} diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/vendor/.gitkeep b/dev-packages/e2e-tests/test-applications/ember-classic/vendor/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/.editorconfig b/dev-packages/e2e-tests/test-applications/ember-embroider/.editorconfig new file mode 100644 index 000000000000..c35a002406b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/.ember-cli b/dev-packages/e2e-tests/test-applications/ember-embroider/.ember-cli new file mode 100644 index 000000000000..4ccb4bf43700 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/.ember-cli @@ -0,0 +1,15 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": false, + + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/.gitignore b/dev-packages/e2e-tests/test-applications/ember-embroider/.gitignore new file mode 100644 index 000000000000..f1e859b291c4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/.gitignore @@ -0,0 +1,32 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/.env* +/.pnp* +/.sass-cache +/.eslintcache +/connect.lock +/coverage/ +/libpeerconnection.log +/npm-debug.log* +/testem.log +/yarn-error.log + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/npm-shrinkwrap.json.ember-try +/package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try + +# broccoli-debug +/DEBUG/ diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/.npmrc b/dev-packages/e2e-tests/test-applications/ember-embroider/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/.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/ember-embroider/.watchmanconfig b/dev-packages/e2e-tests/test-applications/ember-embroider/.watchmanconfig new file mode 100644 index 000000000000..e7834e3e4f39 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/README.md b/dev-packages/e2e-tests/test-applications/ember-embroider/README.md new file mode 100644 index 000000000000..c2ddb5f5357a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/README.md @@ -0,0 +1,56 @@ +# ember-embroider + +This README outlines the details of collaborating on this Ember application. A short introduction of this app could +easily go here. + +## Prerequisites + +You will need the following things properly installed on your computer. + +- [Git](https://git-scm.com/) +- [Node.js](https://nodejs.org/) (with npm) +- [Ember CLI](https://cli.emberjs.com/release/) +- [Google Chrome](https://google.com/chrome/) + +## Installation + +- `git clone ` this repository +- `cd ember-embroider` +- `npm install` + +## Running / Development + +- `ember serve` +- Visit your app at [http://localhost:4200](http://localhost:4200). +- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Running Tests + +- `ember test` +- `ember test --server` + +### Linting + +- `npm run lint` +- `npm run lint:fix` + +### Building + +- `ember build` (development) +- `ember build --environment production` (production) + +### Deploying + +Specify what it takes to deploy your app. + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) +- [ember-cli](https://cli.emberjs.com/release/) +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts new file mode 100644 index 000000000000..7241d14be133 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts @@ -0,0 +1,18 @@ +import Application from '@ember/application'; +import * as Sentry from '@sentry/ember'; +import config from 'ember-embroider/config/environment'; +import loadInitializers from 'ember-load-initializers'; +import Resolver from 'ember-resolver'; + +Sentry.init({ + replaysSessionSampleRate: 1, + replaysOnErrorSampleRate: 1, + tunnel: `http://localhost:3031/`, // proxy server +}); +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver; +} + +loadInitializers(App, config.modulePrefix); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.hbs new file mode 100644 index 000000000000..754607dd75e0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.hbs @@ -0,0 +1,6 @@ + diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.ts new file mode 100644 index 000000000000..ed3fea334397 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.ts @@ -0,0 +1,10 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +export default class ErrorButtonComponent extends Component { + @action + throwGenericJavascriptError() { + // @ts-expect-error This is fine + this.nonExistentFunction(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/config/environment.d.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/config/environment.d.ts new file mode 100644 index 000000000000..8a8a687909e4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/config/environment.d.ts @@ -0,0 +1,17 @@ +/** + * Type declarations for + * import config from './config/environment' + * + * For now these need to be managed by the developer + * since different ember addons can materialize new entries. + */ +declare const config: { + environment: string; + modulePrefix: string; + podModulePrefix: string; + locationType: 'history' | 'hash' | 'none' | 'auto'; + rootURL: string; + APP: Record; +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/index.html b/dev-packages/e2e-tests/test-applications/ember-embroider/app/index.html new file mode 100644 index 000000000000..f22e112de254 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/index.html @@ -0,0 +1,24 @@ + + + + + EmberClassic + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/router.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/router.ts new file mode 100644 index 000000000000..e13dec6d82c5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/router.ts @@ -0,0 +1,18 @@ +import EmberRouter from '@ember/routing/router'; + +import config from './config/environment'; + +export default class Router extends EmberRouter { + public location = config.locationType; + public rootURL = config.rootURL; +} + +// This is a false positive of the eslint rule +// eslint-disable-next-line array-callback-return +Router.map(function () { + this.route('tracing'); + this.route('replay'); + this.route('slow-loading-route', function () { + this.route('index', { path: '/' }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/index.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/index.ts new file mode 100644 index 000000000000..accaec92e0d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/index.ts @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class IndexRoute extends Route {} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route.ts new file mode 100644 index 000000000000..738a2528ec21 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route.ts @@ -0,0 +1,30 @@ +import Route from '@ember/routing/route'; +import { instrumentRoutePerformance } from '@sentry/ember'; + +class SlowLoadingRouteRoute extends Route { + beforeModel() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 250); + }); + } + + model() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 250); + }); + } + + afterModel() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 250); + }); + } +} + +export default instrumentRoutePerformance(SlowLoadingRouteRoute); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route/index.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route/index.ts new file mode 100644 index 000000000000..626c3e403d08 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route/index.ts @@ -0,0 +1,30 @@ +import Route from '@ember/routing/route'; +import { instrumentRoutePerformance } from '@sentry/ember'; + +class SlowLoadingRouteIndexRoute extends Route { + beforeModel() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 250); + }); + } + + model() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 250); + }); + } + + afterModel() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 250); + }); + } +} + +export default instrumentRoutePerformance(SlowLoadingRouteIndexRoute); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/tracing.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/tracing.ts new file mode 100644 index 000000000000..f6cc69a0ae39 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/tracing.ts @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class TracingRoute extends Route {} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/styles/app.css b/dev-packages/e2e-tests/test-applications/ember-embroider/app/styles/app.css new file mode 100644 index 000000000000..f926764e8b3d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/styles/app.css @@ -0,0 +1,197 @@ +:root { + --primary-fg-color: #6c5fc7; + --button-border-color: #413496; + --foreground-color: #2f2936; + --background-color: #f2f1f3; + --content-border-color: #e2dee6; + --button-background-hover-color: #5b4cc0; +} + +html { + height: 100vh; +} + +body { + background: var(--background-color) url('/assets/images/sentry-pattern-transparent.png'); + background-size: 340px; + background-repeat: repeat; + height: 100%; + margin: 0; + font-family: + Rubik, + Avenir Next, + Helvetica Neue, + sans-serif; + font-size: 16px; + line-height: 24px; + color: var(--foreground-color); +} + +.app { + display: flex; + flex-direction: column; + flex-grow: 1; + align-items: center; +} + +.container { + position: relative; + padding-left: 30px; + padding-right: 30px; + padding-top: 5vh; + width: 100%; + max-width: 740px; + flex: 1; +} + +.box { + background-color: #fff; + border: 0; + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.08), + 0 1px 4px rgba(0, 0, 0, 0.1); + border-radius: 4px; + display: flex; + width: 100%; + margin: 0 0 20px; +} + +.sidebar { + padding-top: 20px; + width: 60px; + background: #564f64; + background-image: linear-gradient(-180deg, rgba(52, 44, 62, 0), rgba(52, 44, 62, 0.5)); + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.1); + border-radius: 4px 0 0 4px; + margin-top: -1px; + margin-bottom: -1px; + text-align: center; + + display: flex; + justify-content: center; + padding-top: 20px; + padding-bottom: 20px; +} + +.logo { + width: 24px; + height: 24px; + background-image: url('/assets/images/sentry-logo.svg'); +} + +.nav { + display: flex; + justify-content: center; + padding: 10px; + padding-top: 20px; + padding-bottom: 0px; +} + +.nav a { + padding-left: 10px; + padding-right: 10px; + font-weight: 500; + text-decoration: none; + color: var(--foreground-color); +} + +.nav a.active { + border-bottom: 4px solid #6c5fc7; +} + +section.content { + flex: 1; + padding-bottom: 40px; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 600; +} + +h3 { + font-size: 24px; + line-height: 1.2; +} + +div.section { + margin-top: 20px; +} + +.section h4 { + margin-bottom: 10px; +} + +.content-container { + padding-left: 40px; + padding-right: 40px; + padding-top: 20px; +} + +.content-container h3, +.content-container h4 { + margin-top: 0px; +} + +.border-bottom { + border-bottom: 1px solid var(--content-border-color); +} + +button { + border-radius: 3px; + font-weight: 600; + padding: 8px 16px; + transition: all 0.1s; + + border: 1px solid transparent; + border-radius: 3px; + font-weight: 600; + padding: 8px 16px; + + -webkit-appearance: button; + cursor: pointer; +} + +button:hover { + text-decoration: none; +} + +button:focus { + outline-offset: -2px; +} + +button.primary { + color: #fff; + background-color: var(--primary-fg-color); + border-color: var(--button-border-color); + + display: inline-block; + + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.08); + + text-transform: none; + overflow: visible; +} + +button.primary:hover { + background-color: var(--button-background-hover-color); + border-color: #204d74; +} + +button.primary:focus { + background: #5b4cc0; + border-color: #3a2f87; + box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.12); + outline: none; +} + +.list-grid { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 10px; +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/application.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/application.hbs new file mode 100644 index 000000000000..4e41d992dc3c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/application.hbs @@ -0,0 +1,21 @@ +
+
+
+ +
+
+

Sentry Instrumented Ember Application

+
+ +
+ {{outlet}} +
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/index.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/index.hbs new file mode 100644 index 000000000000..2d439e9f39e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/index.hbs @@ -0,0 +1,4 @@ +{{page-title "Index"}} +{{outlet}} + + diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route.hbs new file mode 100644 index 000000000000..c24cd68950a9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/index.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/index.hbs new file mode 100644 index 000000000000..cfccec8c94c8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/index.hbs @@ -0,0 +1,5 @@ +{{page-title "Index"}} + +

+ This is a slow loading route! +

diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/loading.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/loading.hbs new file mode 100644 index 000000000000..24638b56a00e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/loading.hbs @@ -0,0 +1 @@ +Loading slow route... diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/tracing.hbs b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/tracing.hbs new file mode 100644 index 000000000000..ee694dc85d89 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/tracing.hbs @@ -0,0 +1,7 @@ +{{page-title "Tracing"}} + +

+ This is a fast loading route. +

+ +Measure Things! diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/config/environment.js b/dev-packages/e2e-tests/test-applications/ember-embroider/config/environment.js new file mode 100644 index 000000000000..37edb5c20697 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/config/environment.js @@ -0,0 +1,64 @@ +'use strict'; + +module.exports = function (environment) { + const ENV = { + modulePrefix: 'ember-embroider', + environment, + rootURL: '/', + locationType: 'history', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + }; + + ENV['@sentry/ember'] = { + sentry: { + tracesSampleRate: 1, + dsn: process.env.E2E_TEST_DSN, + tracePropagationTargets: ['localhost', 'doesntexist.example'], + browserTracingOptions: { + _experiments: { + // This lead to some flaky tests, as that is sometimes logged + enableLongTask: false, + }, + }, + }, + ignoreEmberOnErrorWarning: true, + minimumRunloopQueueDuration: 0, + minimumComponentRenderDuration: 0, + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/config/optional-features.json b/dev-packages/e2e-tests/test-applications/ember-embroider/config/optional-features.json new file mode 100644 index 000000000000..5329dd9913bb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/config/optional-features.json @@ -0,0 +1,7 @@ +{ + "application-template-wrapper": false, + "default-async-observers": true, + "jquery-integration": false, + "template-only-glimmer-components": true, + "no-implicit-route-model": true +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/config/targets.js b/dev-packages/e2e-tests/test-applications/ember-embroider/config/targets.js new file mode 100644 index 000000000000..9f6cc639666e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/config/targets.js @@ -0,0 +1,7 @@ +'use strict'; + +const browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; + +module.exports = { + browsers, +}; diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/ember-cli-build.js b/dev-packages/e2e-tests/test-applications/ember-embroider/ember-cli-build.js new file mode 100644 index 000000000000..0338e9ce1608 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/ember-cli-build.js @@ -0,0 +1,19 @@ +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function (defaults) { + const app = new EmberApp(defaults, { + tests: false, + hinting: false, + }); + + const { Webpack } = require('@embroider/webpack'); + return require('@embroider/compat').compatBuild(app, Webpack, { + staticAddonTrees: true, + staticHelpers: true, + staticModifiers: true, + staticComponents: true, + staticEmberSource: true, + }); +}; diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json new file mode 100644 index 000000000000..1512a07d122c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json @@ -0,0 +1,72 @@ +{ + "name": "ember-embroider", + "version": "0.0.0", + "private": true, + "description": "Small description for ember-embroider goes here", + "repository": "", + "license": "MIT", + "author": "", + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "proxy": "ts-node-script start-event-proxy.ts", + "build": "ember build --environment=production", + "start": "ember serve --prod", + "test": "playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add ember-source@latest && npx playwright install && pnpm build", + "test:assert": "playwright test", + "clean": "npx rimraf node_modules,pnpm-lock.yaml,dist" + }, + "devDependencies": { + "@babel/core": "^7.24.4", + "@babel/plugin-proposal-decorators": "^7.24.1", + "@ember/optional-features": "^2.1.0", + "@ember/string": "^3.1.1", + "@ember/test-helpers": "^3.3.0", + "@embroider/compat": "^3.4.8", + "@embroider/core": "^3.4.8", + "@embroider/webpack": "^4.0.0", + "@glimmer/component": "^1.1.2", + "@glimmer/tracking": "^1.1.2", + "broccoli-asset-rev": "^3.0.0", + "ember-auto-import": "^2.7.2", + "ember-cli": "~5.8.0", + "ember-cli-app-version": "^6.0.1", + "ember-cli-babel": "^8.2.0", + "ember-cli-clean-css": "^3.0.0", + "ember-cli-dependency-checker": "^3.3.2", + "ember-cli-htmlbars": "^6.3.0", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-typescript": "5.3.0", + "ember-fetch": "^8.1.2", + "ember-load-initializers": "^2.1.2", + "ember-modifier": "^4.1.0", + "ember-page-title": "^8.2.3", + "ember-resolver": "^11.0.1", + "ember-source": "~5.8.0", + "loader.js": "^4.7.0", + "tracked-built-ins": "^3.3.0", + "webpack": "^5.91.0", + "@playwright/test": "^1.44.1", + "@sentry/ember": "latest || *", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@tsconfig/ember": "^3.0.6", + "@types/node": "18.18.0", + "@tsconfig/node18": "18.2.4", + "@types/rsvp": "^4.0.9", + "ts-node": "10.9.1", + "typescript": "^5.4.5" + }, + "engines": { + "node": ">= 18" + }, + "ember": { + "edition": "octane" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts new file mode 100644 index 000000000000..6c2442587de4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts @@ -0,0 +1,73 @@ +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 emberPort = 4020; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30_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, + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${emberPort}`, + + /* 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: `pnpm ember serve --path=dist/ --port=${emberPort}`, + port: emberPort, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-logo.svg b/dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-logo.svg new file mode 100644 index 000000000000..bac4e57b7790 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-logo.svg @@ -0,0 +1 @@ +logos diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-pattern-transparent.png b/dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-pattern-transparent.png new file mode 100644 index 000000000000..1f7312b5f6af Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-pattern-transparent.png differ diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/public/robots.txt b/dev-packages/e2e-tests/test-applications/ember-embroider/public/robots.txt new file mode 100644 index 000000000000..f5916452e5ff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/public/robots.txt @@ -0,0 +1,3 @@ +# http://www.robotstxt.org +User-agent: * +Disallow: diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/start-event-proxy.ts new file mode 100644 index 000000000000..7d545da45ca2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'ember-embroider', +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts new file mode 100644 index 000000000000..9171611e42cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('ember-embroider', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + + await page.locator('[data-test-button="Throw Generic Javascript Error"]').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'TypeError', + value: 'this.nonExistentFunction is not a function', + mechanism: { + type: 'instrument', + handled: false, + }, + }, + ], + }, + transaction: 'route:index', + }); +}); + +test('assigns the correct transaction value after a navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorPromise = waitForError('ember-embroider', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/tracing`); + await pageloadTxnPromise; + + await page.getByText('Errors').click(); + + const [_, error] = await Promise.all([ + page.locator('[data-test-button="Throw Generic Javascript Error"]').click(), + errorPromise, + ]); + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'TypeError', + value: 'this.nonExistentFunction is not a function', + mechanism: { + type: 'instrument', + handled: false, + }, + }, + ], + }, + transaction: 'route:index', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts new file mode 100644 index 000000000000..a26bec292650 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts @@ -0,0 +1,279 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('ember-embroider', 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.ember', + }, + }, + transaction: 'route:index', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + const [_, navigationTxn] = await Promise.all([page.getByText('Tracing').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.ember', + }, + }, + transaction: 'route:tracing', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, pageloadTxn, navigationTxn] = await Promise.all([ + page.getByText('Tracing').click(), + pageloadTxnPromise, + navigationTxnPromise, + ]); + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.ember', + }, + }, + transaction: 'route:index', + transaction_info: { + source: 'route', + }, + }); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.ember', + }, + }, + transaction: 'route:tracing', + transaction_info: { + source: 'route', + }, + }); +}); + +test('captures correct spans for navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/tracing`); + await pageloadTxnPromise; + + const [_, navigationTxn] = await Promise.all([page.getByText('Measure Things!').click(), navigationTxnPromise]); + + const traceId = navigationTxn.contexts?.trace?.trace_id; + const spanId = navigationTxn.contexts?.trace?.span_id; + + expect(traceId).toBeDefined(); + expect(spanId).toBeDefined(); + + const spans = navigationTxn.spans || []; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.ember', + }, + }, + transaction: 'route:slow-loading-route.index', + transaction_info: { + source: 'route', + }, + }); + + const transitionSpans = spans.filter(span => span.op === 'ui.ember.transition'); + const beforeModelSpans = spans.filter(span => span.op === 'ui.ember.route.before_model'); + const modelSpans = spans.filter(span => span.op === 'ui.ember.route.model'); + const afterModelSpans = spans.filter(span => span.op === 'ui.ember.route.after_model'); + const renderSpans = spans.filter(span => span.op === 'ui.ember.runloop.render'); + + expect(transitionSpans).toHaveLength(1); + + // We have two spans each there - one for `slow-loading-route` and one for `slow-load-route.index` + expect(beforeModelSpans).toHaveLength(2); + expect(modelSpans).toHaveLength(2); + expect(afterModelSpans).toHaveLength(2); + + // There may be many render spans... + expect(renderSpans.length).toBeGreaterThan(1); + + expect(transitionSpans[0]).toEqual({ + data: { + 'sentry.op': 'ui.ember.transition', + 'sentry.origin': 'auto.ui.ember', + }, + description: 'route:tracing -> route:slow-loading-route.index', + op: 'ui.ember.transition', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + expect(beforeModelSpans).toEqual([ + { + data: { + 'sentry.op': 'ui.ember.route.before_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route', + op: 'ui.ember.route.before_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.op': 'ui.ember.route.before_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route.index', + op: 'ui.ember.route.before_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + + expect(modelSpans).toEqual([ + { + data: { + 'sentry.op': 'ui.ember.route.model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route', + op: 'ui.ember.route.model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.op': 'ui.ember.route.model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route.index', + op: 'ui.ember.route.model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + + expect(afterModelSpans).toEqual([ + { + data: { + 'sentry.op': 'ui.ember.route.after_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route', + op: 'ui.ember.route.after_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.op': 'ui.ember.route.after_model', + 'sentry.origin': 'auto.ui.ember', + 'sentry.source': 'custom', + }, + description: 'slow-loading-route.index', + op: 'ui.ember.route.after_model', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + + expect(renderSpans).toContainEqual({ + data: { + 'sentry.op': 'ui.ember.runloop.render', + 'sentry.origin': 'auto.ui.ember', + }, + description: 'runloop', + op: 'ui.ember.runloop.render', + origin: 'auto.ui.ember', + parent_span_id: spanId, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json new file mode 100644 index 000000000000..919403ddcda8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "extends": "@tsconfig/ember/tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "noEmit": true, + + // The combination of `baseUrl` with `paths` allows Ember's classic package + // layout, which is not resolvable with the Node resolution algorithm, to + // work with TypeScript. + "baseUrl": ".", + "paths": { + "ember-embroider/*": [ + "app/*" + ], + "*": [ + "types/*" + ], + } + }, + "include": [ + "app/**/*", + "types/**/*" + ], + "exclude": ["tests/**/*"], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } + +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.json b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.json new file mode 100644 index 000000000000..78f134a16dca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/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/ember-embroider/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.node.json new file mode 100644 index 000000000000..65950d5c2bf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json", + "include": ["playwright.config.*", "start-event-proxy.ts"], + "compilerOptions": { + "composite": true, + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Node", + "types": ["node"] + } +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/types/ember-embroider/index.d.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/types/ember-embroider/index.d.ts new file mode 100644 index 000000000000..d2f5fc1b01a7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/types/ember-embroider/index.d.ts @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +declare global { + // Prevents ESLint from "fixing" this via its auto-fix to turn it into a type + // alias (e.g. after running any Ember CLI generator) + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Array extends Ember.ArrayPrototypeExtensions {} + // interface Function extends Ember.FunctionPrototypeExtensions {} +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/types/global.d.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/types/global.d.ts new file mode 100644 index 000000000000..55d63f8da3a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/types/global.d.ts @@ -0,0 +1,7 @@ +// Types for compiled templates +declare module 'ember-embroider/templates/*' { + import { TemplateFactory } from 'ember-cli-htmlbars'; + + const tmpl: TemplateFactory; + export default tmpl; +} diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/vendor/.gitkeep b/dev-packages/e2e-tests/test-applications/ember-embroider/vendor/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts index 154f62ada912..5ba6bcb2a68e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts @@ -69,6 +69,16 @@ export class AppController1 { async testOutgoingHttpExternalDisallowed() { return this.appService.testOutgoingHttpExternalDisallowed(); } + + @Get('test-span-decorator-async') + async testSpanDecoratorAsync() { + return { result: await this.appService.testSpanDecoratorAsync() }; + } + + @Get('test-span-decorator-sync') + async testSpanDecoratorSync() { + return { result: await this.appService.testSpanDecoratorSync() }; + } } @Controller() diff --git a/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts index 1103c65941a1..7e0df6b7e1c8 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts @@ -1,5 +1,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import * as Sentry from '@sentry/nestjs'; +import { SentryTraced } from '@sentry/nestjs'; import { makeHttpRequest } from './utils'; @Injectable() @@ -75,6 +76,25 @@ export class AppService1 { async testOutgoingHttpExternalDisallowed() { return makeHttpRequest('http://localhost:3040/external-disallowed'); } + + @SentryTraced('wait and return a string') + async wait() { + await new Promise(resolve => setTimeout(resolve, 500)); + return 'test'; + } + + async testSpanDecoratorAsync() { + return await this.wait(); + } + + @SentryTraced('return a string') + getString(): string { + return 'test'; + } + + async testSpanDecoratorSync() { + return this.getString(); + } } @Injectable() diff --git a/dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts new file mode 100644 index 000000000000..3efdfb979d73 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-async' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-async`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'wait and return a string', + 'otel.kind': 'INTERNAL', + }, + description: 'wait', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'wait and return a string', + origin: 'manual', + }), + ]), + ); +}); + +test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-sync' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-sync`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'return a string', + 'otel.kind': 'INTERNAL', + }, + description: 'getString', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'return a string', + origin: 'manual', + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts index aa868ceab291..031fa71ec581 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts @@ -1,11 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('node-connect', transactionEvent => { return ( diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/package.json b/dev-packages/e2e-tests/test-applications/node-hapi/package.json index a6092edbc5ce..47db96a4a666 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi/package.json @@ -11,7 +11,8 @@ "test:assert": "pnpm test" }, "dependencies": { - "@hapi/hapi": "21.3.2", + "@hapi/boom": "10.0.1", + "@hapi/hapi": "21.3.10", "@sentry/node": "latest || *", "@sentry/types": "latest || *" }, diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js index 273cb2f09471..ae803aa3edf7 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-hapi/src/app.js @@ -10,6 +10,7 @@ Sentry.init({ }); const Hapi = require('@hapi/hapi'); +const Boom = require('@hapi/boom'); const server = Hapi.server({ port: 3030, @@ -63,6 +64,55 @@ const init = async () => { throw new Error('This is an error'); }, }); + + server.route({ + method: 'GET', + path: '/test-failure-boom-4xx', + handler: async function (request, h) { + throw new Error('This is a JS error (boom in onPreResponse)'); + }, + }); + + server.route({ + method: 'GET', + path: '/test-failure-boom-5xx', + handler: async function (request, h) { + throw new Error('This is an error (boom in onPreResponse)'); + }, + }); + + server.route({ + method: 'GET', + path: '/test-failure-JS-error-onPreResponse', + handler: async function (request, h) { + throw new Error('This is an error (another JS error in onPreResponse)'); + }, + }); + + server.route({ + method: 'GET', + path: '/test-failure-2xx-override-onPreResponse', + handler: async function (request, h) { + throw new Error('This is a JS error (2xx override in onPreResponse)'); + }, + }); + + // This runs after the route handler + server.ext('onPreResponse', (request, h) => { + const path = request.route.path; + + if (path.includes('boom-4xx')) { + throw Boom.notFound('4xx not found (onPreResponse)'); + } else if (path.includes('boom-5xx')) { + throw Boom.gatewayTimeout('5xx not implemented (onPreResponse)'); + } else if (path.includes('JS-error-onPreResponse')) { + throw new Error('JS error (onPreResponse)'); + } else if (path.includes('2xx-override-onPreResponse')) { + return h.response('2xx override').code(200); + } else { + return h.continue; + } + }); }; (async () => { diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts index 4fb76c1df687..f28250f26f3c 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/errors.test.ts @@ -53,3 +53,97 @@ test('sends error with parameterized transaction name', async ({ baseURL }) => { expect(errorEvent?.transaction).toBe('GET /test-error/{id}'); }); + +test('Does not send errors to Sentry if boom throws in "onPreResponse" after JS error in route handler', async ({ + baseURL, +}) => { + let errorEventOccurred = false; + + waitForError('node-hapi', event => { + if (event.exception?.values?.[0]?.value?.includes('This is a JS error (boom in onPreResponse)')) { + errorEventOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise4xx = waitForTransaction('node-hapi', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-failure-boom-4xx'; + }); + + const transactionEventPromise5xx = waitForTransaction('node-hapi', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-failure-boom-5xx'; + }); + + const response4xx = await fetch(`${baseURL}/test-failure-boom-4xx`); + const response5xx = await fetch(`${baseURL}/test-failure-boom-5xx`); + + expect(response4xx.status).toBe(404); + expect(response5xx.status).toBe(504); + + const transactionEvent4xx = await transactionEventPromise4xx; + const transactionEvent5xx = await transactionEventPromise5xx; + + expect(errorEventOccurred).toBe(false); + expect(transactionEvent4xx.transaction).toBe('GET /test-failure-boom-4xx'); + expect(transactionEvent5xx.transaction).toBe('GET /test-failure-boom-5xx'); +}); + +test('Does not send error to Sentry if error response is overwritten with 2xx in "onPreResponse"', async ({ + baseURL, +}) => { + let errorEventOccurred = false; + + waitForError('node-hapi', event => { + if (event.exception?.values?.[0]?.value?.includes('This is a JS error (2xx override in onPreResponse)')) { + errorEventOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-failure-2xx-override-onPreResponse'; + }); + + const response = await fetch(`${baseURL}/test-failure-2xx-override-onPreResponse`); + + const transactionEvent = await transactionEventPromise; + + expect(response.status).toBe(200); + expect(errorEventOccurred).toBe(false); + expect(transactionEvent.transaction).toBe('GET /test-failure-2xx-override-onPreResponse'); +}); + +test('Only sends onPreResponse error to Sentry if JS error is thrown in route handler AND onPreResponse', async ({ + baseURL, +}) => { + const errorEventPromise = waitForError('node-hapi', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value?.includes('JS error (onPreResponse)') || false; + }); + + let routeHandlerErrorOccurred = false; + + waitForError('node-hapi', event => { + if ( + !event.type && + event.exception?.values?.[0]?.value?.includes('This is an error (another JS error in onPreResponse)') + ) { + routeHandlerErrorOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('node-hapi', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-failure-JS-error-onPreResponse'; + }); + + const response = await fetch(`${baseURL}/test-failure-JS-error-onPreResponse`); + + expect(response.status).toBe(500); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; + + expect(routeHandlerErrorOccurred).toBe(false); + expect(transactionEvent.transaction).toBe('GET /test-failure-JS-error-onPreResponse'); + expect(errorEvent.transaction).toEqual('GET /test-failure-JS-error-onPreResponse'); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts index 64e98f8e75d8..4fd0c72ffd54 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts @@ -77,6 +77,25 @@ test('Sends successful transaction', async ({ baseURL }) => { timestamp: expect.any(Number), trace_id: expect.any(String), }, + { + // this comes from "onPreResponse" + data: { + 'hapi.type': 'server.ext', + 'otel.kind': 'INTERNAL', + 'sentry.op': 'server.ext.hapi', + 'sentry.origin': 'auto.http.otel.hapi', + 'server.ext.type': 'onPreResponse', + }, + description: 'ext - onPreResponse', + op: 'server.ext.hapi', + origin: 'auto.http.otel.hapi', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, ]); }); diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx index 6f6bb0640e73..6b721b0161f0 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -6,9 +6,7 @@ import Index from './pages/Index'; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions - dsn: - process.env.REACT_APP_E2E_TEST_DSN || - 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + dsn: process.env.REACT_APP_E2E_TEST_DSN, release: 'e2e-test', tunnel: 'http://localhost:3031/', // proxy server }); 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 6340fca3f04b..b69dbff7dc48 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 @@ -39,27 +39,6 @@ Sentry.init({ tunnel: 'http://localhost:3031', // proxy server }); -Object.defineProperty(window, 'sentryReplayId', { - get() { - return replay['_replay'].session.id; - }, -}); - -Sentry.addEventProcessor(event => { - if ( - event.type === 'transaction' && - (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') - ) { - const eventId = event.event_id; - if (eventId) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push(eventId); - } - } - - return event; -}); - const useSentryRoutes = Sentry.wrapUseRoutes(useRoutes); function App() { diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 2db716ce5586..6bb7db47474e 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.552.0", - "@hapi/hapi": "^20.3.0", + "@hapi/hapi": "^21.3.10", "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index e95e63700c09..410d0847d928 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -22,7 +22,7 @@ import { makeSetSDKSourcePlugin, makeSucrasePlugin, } from './plugins/index.mjs'; -import { makePackageNodeEsm, makeReactEsmJsxRuntimePlugin } from './plugins/make-esm-plugin.mjs'; +import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs'; import { mergePlugins } from './utils.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -143,7 +143,7 @@ export function makeNPMConfigVariants(baseConfig, options = {}) { output: { format: 'esm', dir: path.join(baseConfig.output.dir, 'esm'), - plugins: [makePackageNodeEsm(), makeReactEsmJsxRuntimePlugin()], + plugins: [makePackageNodeEsm()], }, }); } diff --git a/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs b/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs index 91aba689f888..04dd68beaa1c 100644 --- a/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs +++ b/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs @@ -1,5 +1,4 @@ import fs from 'node:fs'; -import replacePlugin from '@rollup/plugin-replace'; /** * Outputs a package.json file with {type: module} in the root of the output directory so that Node @@ -30,17 +29,3 @@ export function makePackageNodeEsm() { }, }; } - -/** - * Makes sure that whenever we add an `react/jsx-runtime` import, we add a `.js` to make the import esm compatible. - */ -export function makeReactEsmJsxRuntimePlugin() { - return replacePlugin({ - preventAssignment: false, - sourceMap: true, - values: { - "'react/jsx-runtime'": "'react/jsx-runtime.js'", - '"react/jsx-runtime"': '"react/jsx-runtime.js"', - }, - }); -} diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 30bedadc38bb..a6f0822da0fa 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -1,3 +1,5 @@ +/* eslint-disable max-lines */ + import * as fs from 'fs'; import * as http from 'http'; import type { AddressInfo } from 'net'; @@ -17,7 +19,7 @@ interface EventProxyServerOptions { /** The name for the proxy server used for referencing it with listener functions */ proxyServerName: string; /** - * Whether or not to forward the event to sentry. @default `true` + * Whether or not to forward the event to sentry. @default `false` * This is helpful when you can't register a tunnel in the SDK setup (e.g. lambda layer without Sentry.init call) */ forwardToSentry?: boolean; @@ -30,12 +32,22 @@ interface SentryRequestCallbackData { sentryResponseStatusCode?: number; } +interface EventCallbackListener { + (data: string): void; +} + type OnRequest = ( - eventCallbackListeners: Set<(data: string) => void>, + eventCallbackListeners: Set, proxyRequest: http.IncomingMessage, proxyRequestBody: string, + eventBuffer: BufferedEvent[], ) => Promise<[number, string, Record | undefined]>; +interface BufferedEvent { + timestamp: number; + data: string; +} + /** * Start a generic proxy server. * The `onRequest` callback receives the incoming request and the request body, @@ -51,7 +63,8 @@ export async function startProxyServer( }, onRequest?: OnRequest, ): Promise { - const eventCallbackListeners: Set<(data: string) => void> = new Set(); + const eventBuffer: BufferedEvent[] = []; + const eventCallbackListeners: Set = new Set(); const proxyServer = http.createServer((proxyRequest, proxyResponse) => { const proxyRequestChunks: Uint8Array[] = []; @@ -76,7 +89,9 @@ export async function startProxyServer( const callback: OnRequest = onRequest || - (async (eventCallbackListeners, proxyRequest, proxyRequestBody) => { + (async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { + eventBuffer.push({ data: proxyRequestBody, timestamp: Date.now() }); + eventCallbackListeners.forEach(listener => { listener(proxyRequestBody); }); @@ -84,7 +99,7 @@ export async function startProxyServer( return [200, '{}', {}]; }); - callback(eventCallbackListeners, proxyRequest, proxyRequestBody) + callback(eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) .then(([statusCode, responseBody, responseHeaders]) => { proxyResponse.writeHead(statusCode, responseHeaders); proxyResponse.write(responseBody, 'utf-8'); @@ -110,12 +125,24 @@ export async function startProxyServer( eventCallbackResponse.statusCode = 200; eventCallbackResponse.setHeader('connection', 'keep-alive'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const searchParams = new URL(eventCallbackRequest.url!, 'http://justsomerandombasesothattheurlisparseable.com/') + .searchParams; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const listenerTimestamp = Number(searchParams.get('timestamp')!); + const callbackListener = (data: string): void => { eventCallbackResponse.write(data.concat('\n'), 'utf8'); }; eventCallbackListeners.add(callbackListener); + eventBuffer.forEach(bufferedEvent => { + if (bufferedEvent.timestamp >= listenerTimestamp) { + callbackListener(bufferedEvent.data); + } + }); + eventCallbackRequest.on('close', () => { eventCallbackListeners.delete(callbackListener); }); @@ -142,10 +169,10 @@ export async function startProxyServer( * option to this server (like this `tunnel: http://localhost:${port option}/`). */ export async function startEventProxyServer(options: EventProxyServerOptions): Promise { - await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody) => { + await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0] as string); - const shouldForwardEventToSentry = options.forwardToSentry != null ? options.forwardToSentry : true; + const shouldForwardEventToSentry = options.forwardToSentry || false; if (!envelopeHeader.dsn && shouldForwardEventToSentry) { // eslint-disable-next-line no-console @@ -168,7 +195,13 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P listener(Buffer.from(JSON.stringify(data)).toString('base64')); }); - return [200, '{}', {}]; + return [ + 200, + '{}', + { + 'Access-Control-Allow-Origin': '*', + }, + ]; } const { origin, pathname, host } = new URL(envelopeHeader.dsn as string); @@ -199,8 +232,12 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P sentryResponseStatusCode: res.status, }; + const dataString = Buffer.from(JSON.stringify(data)).toString('base64'); + + eventBuffer.push({ data: dataString, timestamp: Date.now() }); + eventCallbackListeners.forEach(listener => { - listener(Buffer.from(JSON.stringify(data)).toString('base64')); + listener(dataString); }); const resHeaders: Record = {}; @@ -221,24 +258,28 @@ export async function waitForPlainRequest( 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); - }); + const request = http.request( + `http://localhost:${eventCallbackServerPort}/?timestamp=${Date.now()}`, + {}, + response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); - response.on('data', (chunk: Buffer) => { - const chunkString = chunk.toString('utf8'); + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); - eventContents = eventContents.concat(chunkString); + eventContents = eventContents.concat(chunkString); - if (callback(eventContents)) { - response.destroy(); - return resolve(eventContents); - } - }); - }); + if (callback(eventContents)) { + response.destroy(); + return resolve(eventContents); + } + }); + }, + ); request.end(); }); @@ -248,48 +289,53 @@ export async function waitForPlainRequest( export async function waitForRequest( proxyServerName: string, callback: (eventData: SentryRequestCallbackData) => Promise | boolean, + timestamp: number = Date.now(), ): 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); - }); + const request = http.request( + `http://localhost:${eventCallbackServerPort}/?timestamp=${timestamp}`, + {}, + 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; - }, + 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'), ); - } else if (callbackResult) { - response.destroy(); - resolve(eventCallbackData); + 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); } - eventContents = ''; - } else { - eventContents = eventContents.concat(char); - } + }); }); - }); - }); + }, + ); request.end(); }); @@ -299,18 +345,23 @@ export async function waitForRequest( export function waitForEnvelopeItem( proxyServerName: string, callback: (envelopeItem: EnvelopeItem) => Promise | boolean, + timestamp: number = Date.now(), ): 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; + 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); + return false; + }, + timestamp, + ).catch(reject); }); } @@ -319,15 +370,20 @@ export function waitForError( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, ): Promise { + const timestamp = Date.now(); return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); + waitForEnvelopeItem( + proxyServerName, + async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }, + timestamp, + ).catch(reject); }); } @@ -336,15 +392,20 @@ export function waitForTransaction( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, ): Promise { + const timestamp = Date.now(); return new Promise((resolve, reject) => { - waitForEnvelopeItem(proxyServerName, async envelopeItem => { - const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); - return true; - } - return false; - }).catch(reject); + waitForEnvelopeItem( + proxyServerName, + async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }, + timestamp, + ).catch(reject); }); } diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index c09bdfa45eae..4479c2e69590 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -51,6 +51,7 @@ export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegra const script = WINDOW.document.createElement('script'); script.src = url; script.crossOrigin = 'anonymous'; + script.referrerPolicy = 'origin'; const waitForLoad = new Promise((resolve, reject) => { script.addEventListener('load', () => resolve()); diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index a27afa1cae15..2681bf95fc5d 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -33,6 +33,7 @@ export class AsyncContextStack { assignedIsolationScope = isolationScope; } + // scope stack for domains or the process this._stack = [{ scope: assignedScope }]; this._isolationScope = assignedIsolationScope; } @@ -90,13 +91,6 @@ export class AsyncContextStack { return this._isolationScope; } - /** - * Returns the scope stack for domains or the process. - */ - public getStack(): Layer[] { - return this._stack; - } - /** * Returns the topmost scope layer in the order domain > local > process. */ @@ -110,7 +104,7 @@ export class AsyncContextStack { private _pushScope(): ScopeInterface { // We want to clone the content of prev scope const scope = this.getScope().clone(); - this.getStack().push({ + this._stack.push({ client: this.getClient(), scope, }); @@ -121,8 +115,8 @@ export class AsyncContextStack { * Pop a scope from the stack. */ private _popScope(): boolean { - if (this.getStack().length <= 1) return false; - return !!this.getStack().pop(); + if (this._stack.length <= 1) return false; + return !!this._stack.pop(); } } diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 59afda8dc43b..150f92388875 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -390,37 +390,40 @@ export abstract class BaseClient implements Client { /* eslint-disable @typescript-eslint/unified-signatures */ /** @inheritdoc */ - public on(hook: 'spanStart', callback: (span: Span) => void): void; + public on(hook: 'spanStart', callback: (span: Span) => void): () => void; /** @inheritdoc */ - public on(hook: 'spanEnd', callback: (span: Span) => void): void; + public on(hook: 'spanEnd', callback: (span: Span) => void): () => void; /** @inheritdoc */ - public on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): void; + public on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): () => void; /** @inheritdoc */ - public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void; + public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): () => void; /** @inheritdoc */ - public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): void; + public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): () => void; /** @inheritdoc */ - public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): void; + public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): () => void; /** @inheritdoc */ - public on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): void; + public on( + hook: 'afterSendEvent', + callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void, + ): () => void; /** @inheritdoc */ - public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void; + public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): () => void; /** @inheritdoc */ - public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): void; + public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): () => void; /** @inheritdoc */ public on( hook: 'beforeSendFeedback', callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void, - ): void; + ): () => void; /** @inheritdoc */ public on( @@ -443,23 +446,41 @@ export abstract class BaseClient implements Client { options: StartSpanOptions, traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, ) => void, - ): void; + ): () => void; /** @inheritdoc */ - public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void; - public on(hook: 'flush', callback: () => void): void; + public on(hook: 'flush', callback: () => void): () => void; - public on(hook: 'close', callback: () => void): void; + public on(hook: 'close', callback: () => void): () => void; /** @inheritdoc */ - public on(hook: string, callback: unknown): void { + public on(hook: string, callback: unknown): () => void { + // Note that the code below, with nullish coalescing assignment, + // may reduce the code, so it may be switched to when Node 14 support + // is dropped (the `??=` operator is supported since Node 15). + // (this._hooks[hook] ??= []).push(callback); if (!this._hooks[hook]) { this._hooks[hook] = []; } // @ts-expect-error We assue the types are correct this._hooks[hook].push(callback); + + // This function returns a callback execution handler that, when invoked, + // deregisters a callback. This is crucial for managing instances where callbacks + // need to be unregistered to prevent self-referencing in callback closures, + // ensuring proper garbage collection. + return () => { + const hooks = this._hooks[hook]; + + if (hooks) { + // @ts-expect-error We assue the types are correct + const cbIndex = hooks.indexOf(callback); + hooks.splice(cbIndex, 1); + } + }; } /** @inheritdoc */ @@ -768,12 +789,18 @@ export abstract class BaseClient implements Client { return prepared; } - const result = processBeforeSend(options, prepared, hint); + const result = processBeforeSend(this, options, prepared, hint); return _validateBeforeSendResult(result, beforeSendLabel); }) .then(processedEvent => { if (processedEvent === null) { this.recordDroppedEvent('before_send', dataCategory, event); + if (isTransactionEvent(event)) { + const spans = event.spans || []; + // the transaction itself counts as one span, plus all the child spans that are added + const spanCount = 1 + spans.length; + this._outcomes['span'] = (this._outcomes['span'] || 0) + spanCount; + } throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log'); } @@ -893,6 +920,7 @@ function _validateBeforeSendResult( * Process the matching `beforeSendXXX` callback. */ function processBeforeSend( + client: Client, options: ClientOptions, event: Event, hint: EventHint, @@ -910,6 +938,8 @@ function processBeforeSend( const processedSpan = beforeSendSpan(span); if (processedSpan) { processedSpans.push(processedSpan); + } else { + client.recordDroppedEvent('before_send', 'span'); } } event.spans = processedSpans; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8ea4b032d44c..1971bb8c94bd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -78,6 +78,7 @@ export { getRootSpan, getActiveSpan, addChildSpanToSpan, + spanTimeInputToSeconds, } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 0878f0b383b3..266a0035b382 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -69,7 +69,7 @@ export function spanToTraceHeader(span: Span): string { } /** - * Convert a span time input intp a timestamp in seconds. + * Convert a span time input into a timestamp in seconds. */ export function spanTimeInputToSeconds(input: SpanTimeInput | undefined): number { if (typeof input === 'number') { diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index d24dd42cb1c9..3fa10828e32a 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -2014,4 +2014,56 @@ describe('BaseClient', () => { }); }); }); + + describe('hook removal with `on`', () => { + it('should return a cleanup function that, when executed, unregisters a hook', async () => { + expect.assertions(8); + + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableSend: true, + }), + ); + + const mockSend = jest.spyOn(client.getTransport()!, 'send').mockImplementation(() => { + return Promise.resolve({ statusCode: 200 }); + }); + + const errorEvent: Event = { message: 'error' }; + + const callback = jest.fn(); + const removeAfterSendEventListenerFn = client.on('afterSendEvent', callback); + + expect(client['_hooks']['afterSendEvent']).toEqual([callback]); + + client.sendEvent(errorEvent); + jest.runAllTimers(); + // Wait for two ticks + // note that for whatever reason, await new Promise(resolve => setTimeout(resolve, 0)) causes the test to hang + await undefined; + await undefined; + + expect(mockSend).toBeCalledTimes(1); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith(errorEvent, { statusCode: 200 }); + + // Should unregister `afterSendEvent` callback. + removeAfterSendEventListenerFn(); + expect(client['_hooks']['afterSendEvent']).toEqual([]); + + client.sendEvent(errorEvent); + jest.runAllTimers(); + // Wait for two ticks + // note that for whatever reason, await new Promise(resolve => setTimeout(resolve, 0)) causes the test to hang + await undefined; + await undefined; + + expect(mockSend).toBeCalledTimes(2); + // Note that the `callback` has still been called only once and not twice, + // because we unregistered it. + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith(errorEvent, { statusCode: 200 }); + }); + }); }); diff --git a/packages/deno/package.json b/packages/deno/package.json index 4451a454cea2..36bdd552173f 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -41,7 +41,7 @@ "build:types": "run-s deno-types build:types:tsc build:types:bundle", "build:types:tsc": "tsc -p tsconfig.types.json", "build:types:bundle": "rollup -c rollup.types.config.mjs", - "build:tarball": "node ./scripts/prepack.js && npm pack", + "build:tarball": "node ./scripts/prepack.js && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build build-types build-test coverage sentry-deno-*.tgz", "prefix": "yarn deno-types", @@ -55,7 +55,7 @@ "test:types": "deno check ./build/index.mjs", "test:unit": "deno test --allow-read --allow-run", "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update", - "yalc:publish": "node ./scripts/prepack.js && yalc publish --push --sig" + "yalc:publish": "node ./scripts/prepack.js && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/deno/scripts/prepack.js b/packages/deno/scripts/prepack.js index 19422f912715..6c7db2bc9878 100644 --- a/packages/deno/scripts/prepack.js +++ b/packages/deno/scripts/prepack.js @@ -18,6 +18,8 @@ const ENTRY_POINTS = ['main', 'module', 'types', 'browser']; const EXPORT_MAP_ENTRY_POINT = 'exports'; const TYPES_VERSIONS_ENTRY_POINT = 'typesVersions'; +const ASSETS = ['README.md', 'LICENSE', 'package.json', '.npmignore']; + const PACKAGE_JSON = 'package.json'; /** @@ -52,11 +54,25 @@ if (!fs.existsSync(path.resolve(BUILD_DIR))) { process.exit(1); } +const buildDirContents = fs.readdirSync(path.resolve(BUILD_DIR)); + +// copy non-code assets to build dir +ASSETS.forEach(asset => { + const assetPath = path.resolve(asset); + if (fs.existsSync(assetPath)) { + const destinationPath = path.resolve(BUILD_DIR, path.basename(asset)); + console.log(`Copying ${path.basename(asset)} to ${path.relative('../..', destinationPath)}.`); + fs.copyFileSync(assetPath, destinationPath); + } +}); + // package.json modifications +const newPackageJsonPath = path.resolve(BUILD_DIR, PACKAGE_JSON); + /** * @type {PackageJson} */ -const newPkgJson = { ...pkgJson }; +const newPkgJson = require(newPackageJsonPath); // modify entry points to point to correct paths (i.e. strip out the build directory) ENTRY_POINTS.filter(entryPoint => newPkgJson[entryPoint]).forEach(entryPoint => { @@ -100,7 +116,7 @@ if (newPkgJson[TYPES_VERSIONS_ENTRY_POINT]) { }); } -const newPackageJsonPath = path.resolve(BUILD_DIR, PACKAGE_JSON); +newPkgJson.files = buildDirContents; // write modified package.json to file (pretty-printed with 2 spaces) try { diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md index 58ab6bc95372..8928327b1470 100644 --- a/packages/nestjs/README.md +++ b/packages/nestjs/README.md @@ -38,6 +38,24 @@ Sentry.init({ Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**. +## Span Decorator + +Use the @SentryTraced() decorator to gain additional performance insights for any function within your NestJS +application. + +```js +import { Injectable } from '@nestjs/common'; +import { SentryTraced } from '@sentry/nestjs'; + +@Injectable() +export class ExampleService { + @SentryTraced('example function') + async performTask() { + // Your business logic here + } +} +``` + ## Links - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nestjs/) diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index 6ac8d97b4241..668187a21e29 100644 --- a/packages/nestjs/src/index.ts +++ b/packages/nestjs/src/index.ts @@ -1,3 +1,5 @@ export * from '@sentry/node'; export { init } from './sdk'; + +export { SentryTraced } from './span-decorator'; diff --git a/packages/nestjs/src/span-decorator.ts b/packages/nestjs/src/span-decorator.ts new file mode 100644 index 000000000000..c56056a26621 --- /dev/null +++ b/packages/nestjs/src/span-decorator.ts @@ -0,0 +1,25 @@ +import { startSpan } from '@sentry/node'; + +/** + * A decorator usable to wrap arbitrary functions with spans. + */ +export function SentryTraced(op: string = 'function') { + return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalMethod = descriptor.value as (...args: any[]) => Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return startSpan( + { + op: op, + name: propertyKey, + }, + async () => { + return originalMethod.apply(this, args); + }, + ); + }; + return descriptor; + }; +} diff --git a/packages/node/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts index 1303464c5374..be452636bef8 100644 --- a/packages/node/src/integrations/tracing/hapi/index.ts +++ b/packages/node/src/integrations/tracing/hapi/index.ts @@ -61,7 +61,7 @@ export const hapiErrorPlugin = { register: async function (serverArg: Record) { const server = serverArg as unknown as Server; - server.events.on('request', (request: Request, event: RequestEvent) => { + server.events.on({ name: 'request', channels: ['error'] }, (request: Request, event: RequestEvent) => { if (getIsolationScope() !== getDefaultIsolationScope()) { const route = request.route; if (route && route.path) { diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index d139044a713f..3064de5818aa 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -15,6 +15,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getStatusMessage, + spanTimeInputToSeconds, } from '@sentry/core'; import type { SpanJSON, SpanOrigin, TraceContext, TransactionEvent, TransactionSource } from '@sentry/types'; import { dropUndefinedKeys, logger } from '@sentry/utils'; @@ -22,7 +23,6 @@ import { SENTRY_TRACE_STATE_PARENT_SPAN_ID } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; -import { convertOtelTimeToSeconds } from './utils/convertOtelTimeToSeconds'; import { getRequestSpanData } from './utils/getRequestSpanData'; import type { SpanNode } from './utils/groupSpansWithParents'; import { getLocalParentId } from './utils/groupSpansWithParents'; @@ -176,7 +176,7 @@ function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { function shouldCleanupSpan(span: ReadableSpan, maxStartTimeOffsetSeconds: number): boolean { const cutoff = Date.now() / 1000 - maxStartTimeOffsetSeconds; - return convertOtelTimeToSeconds(span.startTime) < cutoff; + return spanTimeInputToSeconds(span.startTime) < cutoff; } function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { @@ -236,8 +236,8 @@ function createTransactionForOtelSpan(span: ReadableSpan): TransactionEvent { }, }, spans: [], - start_timestamp: convertOtelTimeToSeconds(span.startTime), - timestamp: convertOtelTimeToSeconds(span.endTime), + start_timestamp: spanTimeInputToSeconds(span.startTime), + timestamp: spanTimeInputToSeconds(span.endTime), transaction: description, type: 'transaction', sdkProcessingMetadata: { @@ -294,9 +294,9 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remai data: allData, description, parent_span_id: parentSpanId, - start_timestamp: convertOtelTimeToSeconds(startTime), + start_timestamp: spanTimeInputToSeconds(startTime), // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time - timestamp: convertOtelTimeToSeconds(endTime) || undefined, + timestamp: spanTimeInputToSeconds(endTime) || undefined, status: getStatusMessage(status), // As per protocol, span status is allowed to be undefined op, origin, diff --git a/packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts b/packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts deleted file mode 100644 index 64087aeffc4d..000000000000 --- a/packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** Convert an OTEL time to seconds */ -export function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number { - return seconds + nano / 1_000_000_000; -} diff --git a/packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts b/packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts deleted file mode 100644 index 4f4911cee0cb..000000000000 --- a/packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { convertOtelTimeToSeconds } from '../../src/utils/convertOtelTimeToSeconds'; - -describe('convertOtelTimeToSeconds', () => { - it('works', () => { - expect(convertOtelTimeToSeconds([0, 0])).toEqual(0); - expect(convertOtelTimeToSeconds([1000, 50])).toEqual(1000.00000005); - expect(convertOtelTimeToSeconds([1000, 505])).toEqual(1000.000000505); - }); -}); diff --git a/packages/react/rollup.npm.config.mjs b/packages/react/rollup.npm.config.mjs index e4b28f60a4a6..4014705e5eb4 100644 --- a/packages/react/rollup.npm.config.mjs +++ b/packages/react/rollup.npm.config.mjs @@ -7,7 +7,9 @@ export default makeNPMConfigVariants( external: ['react', 'react/jsx-runtime'], }, sucrase: { - jsxRuntime: 'automatic', // React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + // React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + // but this breaks react 17, so we keep it at `classic` for now + jsxRuntime: 'classic', production: true, // This is needed so that sucrase uses the production jsx runtime (ie `import { jsx } from 'react/jsx-runtime'` instead of `import { jsxDEV as _jsxDEV } from 'react/jsx-dev-runtime'`) }, }), diff --git a/packages/remix/rollup.npm.config.mjs b/packages/remix/rollup.npm.config.mjs index 60779af94f6b..346e043eb0f9 100644 --- a/packages/remix/rollup.npm.config.mjs +++ b/packages/remix/rollup.npm.config.mjs @@ -12,7 +12,9 @@ export default [ }, }, sucrase: { - jsxRuntime: 'automatic', // React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + // React 19 emits a warning if we don't use the newer jsx transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html + // but this breaks react 17, so we keep it at `classic` for now + jsxRuntime: 'classic', production: true, // This is needed so that sucrase uses the production jsx runtime (ie `import { jsx } from 'react/jsx-runtime'` instead of `import { jsxDEV as _jsxDEV } from 'react/jsx-dev-runtime'`) }, }), diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index bb0ec4646211..eed9279352eb 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -181,12 +181,14 @@ export interface Client { /** * Register a callback for whenever a span is started. * Receives the span as argument. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'spanStart', callback: (span: Span) => void): void; + on(hook: 'spanStart', callback: (span: Span) => void): () => void; /** * Register a callback before span sampling runs. Receives a `samplingDecision` object argument with a `decision` * property that can be used to make a sampling decision that will be enforced, before any span sampling runs. + * @returns A function that, when executed, removes the registered callback. */ on( hook: 'beforeSampling', @@ -204,60 +206,70 @@ export interface Client { /** * Register a callback for whenever a span is ended. * Receives the span as argument. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'spanEnd', callback: (span: Span) => void): void; + on(hook: 'spanEnd', callback: (span: Span) => void): () => void; /** * Register a callback for when an idle span is allowed to auto-finish. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): void; + on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): () => void; /** * Register a callback for transaction start and finish. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void; + on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): () => void; /** * Register a callback for before sending an event. * This is called right before an event is sent and should not be used to mutate the event. * Receives an Event & EventHint as arguments. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void; + on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; /** * Register a callback for preprocessing an event, * before it is passed to (global) event processors. * Receives an Event & EventHint as arguments. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void; + on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void; /** * Register a callback for when an event has been sent. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): void; + on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): () => void; /** * Register a callback before a breadcrumb is added. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void; + on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): () => void; /** * Register a callback when a DSC (Dynamic Sampling Context) is created. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): void; + on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): () => void; /** * Register a callback when a Feedback event has been prepared. * This should be used to mutate the event. The options argument can hint * about what kind of mutation it expects. + * @returns A function that, when executed, removes the registered callback. */ on( hook: 'beforeSendFeedback', callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void, - ): void; + ): () => void; /** * A hook for the browser tracing integrations to trigger a span start for a page load. + * @returns A function that, when executed, removes the registered callback. */ on( hook: 'startPageLoadSpan', @@ -265,22 +277,25 @@ export interface Client { options: StartSpanOptions, traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, ) => void, - ): void; + ): () => void; /** * A hook for browser tracing integrations to trigger a span for a navigation. + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void; /** * A hook that is called when the client is flushing + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'flush', callback: () => void): void; + on(hook: 'flush', callback: () => void): () => void; /** * A hook that is called when the client is closing + * @returns A function that, when executed, removes the registered callback. */ - on(hook: 'close', callback: () => void): void; + on(hook: 'close', callback: () => void): () => void; /** Fire a hook whener a span starts. */ emit(hook: 'spanStart', span: Span): void; diff --git a/yarn.lock b/yarn.lock index 035d666481bb..676d0b52bbf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5156,267 +5156,255 @@ resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-2.0.0.tgz#5e8b7298f31ff8f7b260e6b7363c7e9ceed7d9c5" integrity sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA== -"@hapi/accept@^5.0.1": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523" - integrity sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw== - dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" - -"@hapi/ammo@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-5.0.1.tgz#9d34560f5c214eda563d838c01297387efaab490" - integrity sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA== +"@hapi/accept@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab" + integrity sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw== dependencies: - "@hapi/hoek" "9.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" -"@hapi/b64@5.x.x": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d" - integrity sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw== +"@hapi/ammo@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-6.0.1.tgz#1bc9f7102724ff288ca03b721854fc5393ad123a" + integrity sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w== dependencies: - "@hapi/hoek" "9.x.x" + "@hapi/hoek" "^11.0.2" -"@hapi/boom@9.x.x": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.2.tgz#48bd41d67437164a2d636e3b5bc954f8c8dc5e38" - integrity sha512-uJEJtiNHzKw80JpngDGBCGAmWjBtzxDCz17A9NO2zCi8LLBlb5Frpq4pXwyN+2JQMod4pKz5BALwyneCgDg89Q== +"@hapi/b64@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-6.0.1.tgz#786b47dc070e14465af49e2428c1025bd06ed3df" + integrity sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw== dependencies: - "@hapi/hoek" "9.x.x" + "@hapi/hoek" "^11.0.2" -"@hapi/boom@^9.1.0": - version "9.1.4" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6" - integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== +"@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685" + integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA== dependencies: - "@hapi/hoek" "9.x.x" + "@hapi/hoek" "^11.0.2" -"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d" - integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A== +"@hapi/bounce@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-3.0.1.tgz#25a51bf95733749c557c6bf948048bffa66435e4" + integrity sha512-G+/Pp9c1Ha4FDP+3Sy/Xwg2O4Ahaw3lIZFSX+BL4uWi64CmiETuZPxhKDUD4xBMOUZbBlzvO8HjiK8ePnhBadA== dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" -"@hapi/bourne@2.x.x": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.1.0.tgz#66aff77094dc3080bd5df44ec63881f2676eb020" - integrity sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q== +"@hapi/bourne@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7" + integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w== -"@hapi/call@^8.0.0": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0" - integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g== +"@hapi/call@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@hapi/call/-/call-9.0.1.tgz#569b87d5b67abf0e58fb82a3894a61aaed3ca92e" + integrity sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg== dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" -"@hapi/catbox-memory@^5.0.0": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz#cb63fca0ded01d445a2573b38eb2688df67f70ac" - integrity sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ== +"@hapi/catbox-memory@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz#399fa83e85134d45a548eee978e4c3c1523e1a70" + integrity sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g== dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" -"@hapi/catbox@^11.1.1": - version "11.1.1" - resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-11.1.1.tgz#d277e2d5023fd69cddb33d05b224ea03065fec0c" - integrity sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ== +"@hapi/catbox@^12.1.1": + version "12.1.1" + resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-12.1.1.tgz#9339dca0a5b18b3ca0a825ac5dfc916dbc5bab83" + integrity sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw== dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" - "@hapi/podium" "4.x.x" - "@hapi/validate" "1.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" + "@hapi/podium" "^5.0.0" + "@hapi/validate" "^2.0.1" -"@hapi/content@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@hapi/content/-/content-5.0.2.tgz#ae57954761de570392763e64cdd75f074176a804" - integrity sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw== +"@hapi/content@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/content/-/content-6.0.0.tgz#2427af3bac8a2f743512fce2a70cbdc365af29df" + integrity sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA== dependencies: - "@hapi/boom" "9.x.x" + "@hapi/boom" "^10.0.0" -"@hapi/cryptiles@5.x.x": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-5.1.0.tgz#655de4cbbc052c947f696148c83b187fc2be8f43" - integrity sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA== +"@hapi/cryptiles@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-6.0.1.tgz#7868a9d4233567ed66f0a9caf85fdcc56e980621" + integrity sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ== dependencies: - "@hapi/boom" "9.x.x" + "@hapi/boom" "^10.0.1" -"@hapi/file@2.x.x": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9" - integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ== - -"@hapi/hapi@^20.3.0": - version "20.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.3.0.tgz#1d620005afeebcb2c8170352286a4664b0107c15" - integrity sha512-zvPSRvaQyF3S6Nev9aiAzko2/hIFZmNSJNcs07qdVaVAvj8dGJSV4fVUuQSnufYJAGiSau+U5LxMLhx79se5WA== - dependencies: - "@hapi/accept" "^5.0.1" - "@hapi/ammo" "^5.0.1" - "@hapi/boom" "^9.1.0" - "@hapi/bounce" "^2.0.0" - "@hapi/call" "^8.0.0" - "@hapi/catbox" "^11.1.1" - "@hapi/catbox-memory" "^5.0.0" - "@hapi/heavy" "^7.0.1" - "@hapi/hoek" "^9.0.4" - "@hapi/mimos" "^6.0.0" - "@hapi/podium" "^4.1.1" - "@hapi/shot" "^5.0.5" - "@hapi/somever" "^3.0.0" - "@hapi/statehood" "^7.0.3" - "@hapi/subtext" "^7.1.0" - "@hapi/teamwork" "^5.1.0" - "@hapi/topo" "^5.0.0" - "@hapi/validate" "^1.1.1" - -"@hapi/heavy@^7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-7.0.1.tgz#73315ae33b6e7682a0906b7a11e8ca70e3045874" - integrity sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA== +"@hapi/file@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@hapi/file/-/file-3.0.0.tgz#f1fd824493ac89a6fceaf89c824afc5ae2121c09" + integrity sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q== + +"@hapi/hapi@^21.3.10": + version "21.3.10" + resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-21.3.10.tgz#0357db7ca49415e50e5df80ba50ad3964f2a62f3" + integrity sha512-CmEcmTREW394MaGGKvWpoOK4rG8tKlpZLs30tbaBzhCrhiL2Ti/HARek9w+8Ya4nMBGcd+kDAzvU44OX8Ms0Jg== + dependencies: + "@hapi/accept" "^6.0.1" + "@hapi/ammo" "^6.0.1" + "@hapi/boom" "^10.0.1" + "@hapi/bounce" "^3.0.1" + "@hapi/call" "^9.0.1" + "@hapi/catbox" "^12.1.1" + "@hapi/catbox-memory" "^6.0.2" + "@hapi/heavy" "^8.0.1" + "@hapi/hoek" "^11.0.2" + "@hapi/mimos" "^7.0.1" + "@hapi/podium" "^5.0.1" + "@hapi/shot" "^6.0.1" + "@hapi/somever" "^4.1.1" + "@hapi/statehood" "^8.1.1" + "@hapi/subtext" "^8.1.0" + "@hapi/teamwork" "^6.0.0" + "@hapi/topo" "^6.0.1" + "@hapi/validate" "^2.0.1" + +"@hapi/heavy@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-8.0.1.tgz#e2be4a6a249005b5a587f7604aafa8ed02461fb6" + integrity sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w== dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" - "@hapi/validate" "1.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/hoek" "^11.0.2" + "@hapi/validate" "^2.0.1" -"@hapi/hoek@9.x.x": - version "9.2.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" - integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== +"@hapi/hoek@^11.0.2": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.4.tgz#42a7f244fd3dd777792bfb74b8c6340ae9182f37" + integrity sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ== -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/iron@6.x.x": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-6.0.0.tgz#ca3f9136cda655bdd6028de0045da0de3d14436f" - integrity sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw== +"@hapi/iron@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-7.0.1.tgz#f74bace8dad9340c7c012c27c078504f070f14b5" + integrity sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ== dependencies: - "@hapi/b64" "5.x.x" - "@hapi/boom" "9.x.x" - "@hapi/bourne" "2.x.x" - "@hapi/cryptiles" "5.x.x" - "@hapi/hoek" "9.x.x" + "@hapi/b64" "^6.0.1" + "@hapi/boom" "^10.0.1" + "@hapi/bourne" "^3.0.0" + "@hapi/cryptiles" "^6.0.1" + "@hapi/hoek" "^11.0.2" -"@hapi/mimos@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484" - integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg== +"@hapi/mimos@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-7.0.1.tgz#5b65c76bb9da28ba34b0092215891f2c72bc899d" + integrity sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew== dependencies: - "@hapi/hoek" "9.x.x" - mime-db "1.x.x" + "@hapi/hoek" "^11.0.2" + mime-db "^1.52.0" -"@hapi/nigel@4.x.x": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-4.0.2.tgz#8f84ef4bca4fb03b2376463578f253b0b8e863c4" - integrity sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw== +"@hapi/nigel@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-5.0.1.tgz#a6dfe357e9d48d944e2ffc552bd95cb701d79ee9" + integrity sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw== dependencies: - "@hapi/hoek" "^9.0.4" - "@hapi/vise" "^4.0.0" + "@hapi/hoek" "^11.0.2" + "@hapi/vise" "^5.0.1" -"@hapi/pez@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.1.0.tgz#c03a5e01f8be01cfabc4c0017631e619586321c1" - integrity sha512-YfB0btnkLB3lb6Ry/1KifnMPBm5ZPfaAHWFskzOMAgDgXgcBgA+zjpIynyEiBfWEz22DBT8o1e2tAaBdlt8zbw== +"@hapi/pez@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-6.1.0.tgz#64d9f95580fc7d8f1d13437ee4a8676709954fda" + integrity sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg== dependencies: - "@hapi/b64" "5.x.x" - "@hapi/boom" "9.x.x" - "@hapi/content" "^5.0.2" - "@hapi/hoek" "9.x.x" - "@hapi/nigel" "4.x.x" + "@hapi/b64" "^6.0.1" + "@hapi/boom" "^10.0.1" + "@hapi/content" "^6.0.0" + "@hapi/hoek" "^11.0.2" + "@hapi/nigel" "^5.0.1" -"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1": - version "4.1.3" - resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26" - integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g== +"@hapi/podium@^5.0.0", "@hapi/podium@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-5.0.1.tgz#f292b4c0ca3118747394a102c6c3340bda96662f" + integrity sha512-eznFTw6rdBhAijXFIlBOMJJd+lXTvqbrBIS4Iu80r2KTVIo4g+7fLy4NKp/8+UnSt5Ox6mJtAlKBU/Sf5080TQ== dependencies: - "@hapi/hoek" "9.x.x" - "@hapi/teamwork" "5.x.x" - "@hapi/validate" "1.x.x" + "@hapi/hoek" "^11.0.2" + "@hapi/teamwork" "^6.0.0" + "@hapi/validate" "^2.0.1" -"@hapi/shot@^5.0.5": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd" - integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A== +"@hapi/shot@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-6.0.1.tgz#ea84d1810b7c8599d5517c23b4ec55a529d7dc16" + integrity sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA== dependencies: - "@hapi/hoek" "9.x.x" - "@hapi/validate" "1.x.x" + "@hapi/hoek" "^11.0.2" + "@hapi/validate" "^2.0.1" -"@hapi/somever@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.1.tgz#9961cd5bdbeb5bb1edc0b2acdd0bb424066aadcc" - integrity sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w== +"@hapi/somever@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-4.1.1.tgz#b492c78408303c72cd1a39c5060f35d18a404b27" + integrity sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg== dependencies: - "@hapi/bounce" "2.x.x" - "@hapi/hoek" "9.x.x" + "@hapi/bounce" "^3.0.1" + "@hapi/hoek" "^11.0.2" -"@hapi/statehood@^7.0.3": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.4.tgz#6acb9d0817b5c657089356f7d9fd60af0bce4f41" - integrity sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw== - dependencies: - "@hapi/boom" "9.x.x" - "@hapi/bounce" "2.x.x" - "@hapi/bourne" "2.x.x" - "@hapi/cryptiles" "5.x.x" - "@hapi/hoek" "9.x.x" - "@hapi/iron" "6.x.x" - "@hapi/validate" "1.x.x" - -"@hapi/subtext@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-7.1.0.tgz#b4d1ea2aeab1923ac130a24e75921e38fab5b15b" - integrity sha512-n94cU6hlvsNRIpXaROzBNEJGwxC+HA69q769pChzej84On8vsU14guHDub7Pphr/pqn5b93zV3IkMPDU5AUiXA== - dependencies: - "@hapi/boom" "9.x.x" - "@hapi/bourne" "2.x.x" - "@hapi/content" "^5.0.2" - "@hapi/file" "2.x.x" - "@hapi/hoek" "9.x.x" - "@hapi/pez" "^5.1.0" - "@hapi/wreck" "17.x.x" - -"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.1.tgz#4d2ba3cac19118a36c44bf49a3a47674de52e4e4" - integrity sha512-1oPx9AE5TIv+V6Ih54RP9lTZBso3rP8j4Xhb6iSVwPXtAM+sDopl5TFMv5Paw73UnpZJ9gjcrTE1BXrWt9eQrg== +"@hapi/statehood@^8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-8.1.1.tgz#db4bd14c90810a1389763cb0b0b8f221aa4179c1" + integrity sha512-YbK7PSVUA59NArAW5Np0tKRoIZ5VNYUicOk7uJmWZF6XyH5gGL+k62w77SIJb0AoAJ0QdGQMCQ/WOGL1S3Ydow== + dependencies: + "@hapi/boom" "^10.0.1" + "@hapi/bounce" "^3.0.1" + "@hapi/bourne" "^3.0.0" + "@hapi/cryptiles" "^6.0.1" + "@hapi/hoek" "^11.0.2" + "@hapi/iron" "^7.0.1" + "@hapi/validate" "^2.0.1" + +"@hapi/subtext@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-8.1.0.tgz#58733020a6655bc4d978df9e2f75e31696ff3f91" + integrity sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww== + dependencies: + "@hapi/boom" "^10.0.1" + "@hapi/bourne" "^3.0.0" + "@hapi/content" "^6.0.0" + "@hapi/file" "^3.0.0" + "@hapi/hoek" "^11.0.2" + "@hapi/pez" "^6.1.0" + "@hapi/wreck" "^18.0.1" + +"@hapi/teamwork@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-6.0.0.tgz#b3a173cf811ba59fc6ee22318a1b51f4561f06e0" + integrity sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A== -"@hapi/topo@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== +"@hapi/topo@^6.0.1": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-6.0.2.tgz#f219c1c60da8430228af4c1f2e40c32a0d84bbb4" + integrity sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg== dependencies: - "@hapi/hoek" "^9.0.0" + "@hapi/hoek" "^11.0.2" -"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" - integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== +"@hapi/validate@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-2.0.1.tgz#45cf228c4c8cfc61ba2da7e0a5ba93ff3b9afff1" + integrity sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA== dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/topo" "^5.0.0" + "@hapi/hoek" "^11.0.2" + "@hapi/topo" "^6.0.1" -"@hapi/vise@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-4.0.0.tgz#c6a94fe121b94a53bf99e7489f7fcc74c104db02" - integrity sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg== +"@hapi/vise@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-5.0.1.tgz#5c9f16bcf1c039ddd4b6cad5f32d71eeb6bb7dac" + integrity sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A== dependencies: - "@hapi/hoek" "9.x.x" + "@hapi/hoek" "^11.0.2" -"@hapi/wreck@17.x.x": - version "17.2.0" - resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-17.2.0.tgz#a5b69b724fa8fa25550fb02f55c649becfc59f63" - integrity sha512-pJ5kjYoRPYDv+eIuiLQqhGon341fr2bNIYZjuotuPJG/3Ilzr/XtI+JAp0A86E2bYfsS3zBPABuS2ICkaXFT8g== +"@hapi/wreck@^18.0.1": + version "18.1.0" + resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-18.1.0.tgz#68e631fc7568ebefc6252d5b86cb804466c8dbe6" + integrity sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w== dependencies: - "@hapi/boom" "9.x.x" - "@hapi/bourne" "2.x.x" - "@hapi/hoek" "9.x.x" + "@hapi/boom" "^10.0.1" + "@hapi/bourne" "^3.0.0" + "@hapi/hoek" "^11.0.2" "@humanwhocodes/config-array@^0.5.0": version "0.5.0" @@ -24677,7 +24665,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.52.0, mime-db@1.x.x, "mime-db@>= 1.43.0 < 2": +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2", 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==