From 993acd4ee3469234617e25ef2473918d1fbe4711 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 4 Jul 2024 10:44:19 +0200 Subject: [PATCH 01/17] test: Fix e2e test race condition by buffering events (#12739) Co-authored-by: Francesco Novy --- .github/workflows/build.yml | 1 + .../test-utils/src/event-proxy-server.ts | 215 +++++++++++------- 2 files changed, 136 insertions(+), 80 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba49c52fef94..3df961a6c778 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/**' diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 30bedadc38bb..e4eb48f03076 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'; @@ -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,7 +169,7 @@ 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; @@ -199,8 +226,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 +252,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 +283,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 +339,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 +364,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 +386,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); }); } From 77ce28a4fe4d0c561bee274652ed83274e89879a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Jul 2024 11:06:33 +0200 Subject: [PATCH 02/17] test(remix): Update remix E2E tests to avoid sending to Sentry (#12750) Part of https://github.com/getsentry/sentry-javascript/issues/11910 These are the last (non-optional) tests we have that still sent data to Sentry! After that, E2E tests should be completely independent. This also blocks https://github.com/getsentry/sentry-javascript/pull/12743 --- dev-packages/e2e-tests/README.md | 2 + .../tests/server-transactions.test.ts | 5 - .../app/entry.client.tsx | 15 -- .../app/routes/_index.tsx | 3 +- .../server.mjs | 2 +- .../tests/behaviour-client.test.ts | 192 ------------------ .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ .../tests/server-errors.test.ts | 14 ++ ...er.test.ts => server-transactions.test.ts} | 40 +--- .../app/entry.client.tsx | 15 -- .../app/routes/_index.tsx | 3 +- .../server.mjs | 2 +- .../tests/behaviour-client.test.ts | 168 --------------- .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ ...er.test.ts => server-transactions.test.ts} | 14 +- .../app/entry.client.tsx | 15 -- .../app/routes/_index.tsx | 3 +- .../create-remix-app-express/server.mjs | 2 +- .../tests/behaviour-client.test.ts | 192 ------------------ .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ .../tests/server-errors.test.ts | 14 ++ ...er.test.ts => server-transactions.test.ts} | 39 +--- .../app/entry.client.tsx | 15 -- .../app/routes/_index.tsx | 3 +- .../tests/behaviour-client.test.ts | 192 ------------------ .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ ...er.test.ts => server-transactions.test.ts} | 12 +- .../app/entry.client.tsx | 15 -- .../app/routes/_index.tsx | 3 +- .../tests/behaviour-client.test.ts | 192 ------------------ .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ ...er.test.ts => server-transactions.test.ts} | 12 +- .../create-remix-app-v2/app/entry.client.tsx | 15 -- .../create-remix-app-v2/app/routes/_index.tsx | 3 +- .../tests/behaviour-client.test.ts | 192 ------------------ .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ ...er.test.ts => server-transactions.test.ts} | 12 +- .../create-remix-app/app/entry.client.tsx | 15 -- .../create-remix-app/app/routes/_index.tsx | 3 +- .../tests/behaviour-client.test.ts | 192 ------------------ .../tests/client-errors.test.ts | 29 +++ .../tests/client-transactions.test.ts | 57 ++++++ ...er.test.ts => server-transactions.test.ts} | 12 +- .../node-connect/tests/transactions.test.ts | 5 - .../react-router-6-use-routes/src/index.tsx | 21 -- 51 files changed, 665 insertions(+), 1591 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/client-transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/server-errors.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/{behaviour-server.test.ts => server-transactions.test.ts} (77%) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/{behaviour-server.test.ts => server-transactions.test.ts} (79%) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-errors.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/{behaviour-server.test.ts => server-transactions.test.ts} (79%) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/client-transactions.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app-legacy/tests/{behaviour-server.test.ts => server-transactions.test.ts} (80%) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/client-transactions.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app-v2-legacy/tests/{behaviour-server.test.ts => server-transactions.test.ts} (80%) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/client-transactions.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/{behaviour-server.test.ts => server-transactions.test.ts} (81%) delete mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app/tests/client-transactions.test.ts rename dev-packages/e2e-tests/test-applications/create-remix-app/tests/{behaviour-server.test.ts => server-transactions.test.ts} (81%) diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 245f21e8d97a..77d327ba1015 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -90,6 +90,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/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/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/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/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/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() { From 506e6cab2f6ce98567a2db9a3c09c3e393b4e73c Mon Sep 17 00:00:00 2001 From: Artur Androsovych Date: Thu, 4 Jul 2024 10:34:41 +0100 Subject: [PATCH 03/17] feat(core): allow unregistering callback through `on` (#11710) This commit updates the return signature of the client's `on` function to be a void function, which, when executed, unregisters a callback. This adjustment is necessary for managing instances where objects are created and destroyed, ensuring that callbacks are properly unregistered to prevent self-referencing in callback closures and facilitate proper garbage collection. Typically, changing a type from `void` to `() => void` (or `VoidFunction`) shouldn't be considered a breaking change because `void` signifies the absence of a return value, implying that the return value of the `on` function should never be used by consumers. Opting for the `on` approach, which returns a cleanup function, "seems" simpler because having another function called `off` requires saving the callback reference for later removal. With our pattern, we encapsulate both the registration and removal of event listeners within a single function call. --------- Co-authored-by: Francesco Novy --- packages/core/src/baseclient.ts | 51 +++++++++++++++++++--------- packages/core/test/lib/base.test.ts | 52 +++++++++++++++++++++++++++++ packages/types/src/client.ts | 43 ++++++++++++++++-------- 3 files changed, 117 insertions(+), 29 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 59afda8dc43b..70c4261a6d3f 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 */ 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/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; From ef7e5b679ca7828a86d24f2013ee90a0068f7e3c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Jul 2024 13:12:47 +0200 Subject: [PATCH 04/17] fix(tracing): Report dropped spans for transactions (#12751) We already report a client report for dropped standalone spans. However, so far we did not report any for spans dropped for transactions. This PR also emits a client report when `beforeSendSpan` is used to filter a span of a transaction, as well as when the whole transaction is dropped in `beforeSendTransaction`. For now, we _do not_ emit this if users manually drop single spans in e.g. `beforeSendTransaction`, which I'd say is OK for now. Closes https://github.com/getsentry/sentry-javascript/issues/12727 --- packages/core/src/baseclient.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 70c4261a6d3f..150f92388875 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -789,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'); } @@ -914,6 +920,7 @@ function _validateBeforeSendResult( * Process the matching `beforeSendXXX` callback. */ function processBeforeSend( + client: Client, options: ClientOptions, event: Event, hint: EventHint, @@ -931,6 +938,8 @@ function processBeforeSend( const processedSpan = beforeSendSpan(span); if (processedSpan) { processedSpans.push(processedSpan); + } else { + client.recordDroppedEvent('before_send', 'span'); } } event.spans = processedSpans; From dc2659ca1a5c76659c83f3ed2c8f9ba75768d2bb Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Jul 2024 13:13:52 +0200 Subject: [PATCH 05/17] test(ember): Update ember tests to use E2E structure (#11827) This gets rid of the Ember canary tests that are always failing, probably due to ember-try (which we use there) not playing nicely with the monorepo etc. Instead, this now uses the proper E2E setup. I added two tests, for classic ember and modern embroider-based ember. While doing this I also noticed two bugs I fixed along the way :O This also removed the ember canary tests, IMHO the e2e tests are good enough for us there now. --- .github/workflows/build.yml | 2 + .github/workflows/canary.yml | 42 --- .../aws-lambda-layer-cjs/package.json | 2 +- .../ember-classic/.editorconfig | 19 ++ .../ember-classic/.ember-cli | 15 + .../ember-classic/.gitignore | 32 ++ .../test-applications/ember-classic/.npmrc | 2 + .../ember-classic/.watchmanconfig | 3 + .../test-applications/ember-classic/README.md | 56 ++++ .../ember-classic/app/app.ts | 20 ++ .../ember-classic/app/components/link.hbs | 3 + .../ember-classic/app/components/link.ts | 33 +++ .../app/components/slow-loading-gc-list.ts | 4 + .../app/components/slow-loading-list.ts | 25 ++ .../app/components/test-section.ts | 6 + .../ember-classic/app/config/environment.d.ts | 17 ++ .../ember-classic/app/controllers/index.ts | 63 ++++ .../app/controllers/slow-loading-route.ts | 13 + .../controllers/slow-loading-route/index.ts | 5 + .../ember-classic/app/controllers/tracing.ts | 13 + .../ember-classic/app/helpers/utils.ts | 3 + .../ember-classic/app/index.html | 24 ++ .../app/initializers/deprecation.ts | 13 + .../ember-classic/app/router.ts | 18 ++ .../ember-classic/app/routes/replay.ts | 13 + .../app/routes/slow-loading-route.ts | 26 ++ .../app/routes/slow-loading-route/index.ts | 26 ++ .../ember-classic/app/styles/app.css | 197 +++++++++++++ .../app/templates/application.hbs | 22 ++ .../components/slow-loading-gc-list.hbs | 10 + .../components/slow-loading-list.hbs | 10 + .../app/templates/components/test-section.hbs | 6 + .../ember-classic/app/templates/index.hbs | 11 + .../ember-classic/app/templates/replay.hbs | 1 + .../app/templates/slow-loading-route.hbs | 11 + .../templates/slow-loading-route/index.hbs | 5 + .../ember-classic/app/templates/tracing.hbs | 2 + .../config/ember-cli-update.json | 18 ++ .../ember-classic/config/environment.js | 64 ++++ .../config/optional-features.json | 6 + .../ember-classic/config/targets.js | 7 + .../ember-classic/ember-cli-build.js | 24 ++ .../ember-classic/package.json | 83 ++++++ .../ember-classic/playwright.config.ts | 73 +++++ .../public/assets/images/sentry-logo.svg | 1 + .../images/sentry-pattern-transparent.png | Bin 0 -> 28158 bytes .../ember-classic/public/robots.txt | 3 + .../ember-classic/start-event-proxy.ts | 6 + .../ember-classic/tests/errors.test.ts | 66 +++++ .../ember-classic/tests/performance.test.ts | 279 ++++++++++++++++++ .../ember-classic/tsconfig.app.json | 32 ++ .../ember-classic/tsconfig.json | 11 + .../ember-classic/tsconfig.node.json | 11 + .../types/ember-classic/index.d.ts | 11 + .../ember-classic/types/global.d.ts | 7 + .../ember-classic/vendor/.gitkeep | 0 .../ember-embroider/.editorconfig | 19 ++ .../ember-embroider/.ember-cli | 15 + .../ember-embroider/.gitignore | 32 ++ .../test-applications/ember-embroider/.npmrc | 2 + .../ember-embroider/.watchmanconfig | 3 + .../ember-embroider/README.md | 56 ++++ .../ember-embroider/app/app.ts | 18 ++ .../app/components/error-button.hbs | 6 + .../app/components/error-button.ts | 10 + .../app/config/environment.d.ts | 17 ++ .../ember-embroider/app/index.html | 24 ++ .../ember-embroider/app/router.ts | 18 ++ .../ember-embroider/app/routes/index.ts | 3 + .../app/routes/slow-loading-route.ts | 30 ++ .../app/routes/slow-loading-route/index.ts | 30 ++ .../ember-embroider/app/routes/tracing.ts | 3 + .../ember-embroider/app/styles/app.css | 197 +++++++++++++ .../app/templates/application.hbs | 21 ++ .../ember-embroider/app/templates/index.hbs | 4 + .../app/templates/slow-loading-route.hbs | 1 + .../templates/slow-loading-route/index.hbs | 5 + .../templates/slow-loading-route/loading.hbs | 1 + .../ember-embroider/app/templates/tracing.hbs | 7 + .../ember-embroider/config/environment.js | 64 ++++ .../config/optional-features.json | 7 + .../ember-embroider/config/targets.js | 7 + .../ember-embroider/ember-cli-build.js | 19 ++ .../ember-embroider/package.json | 72 +++++ .../ember-embroider/playwright.config.ts | 73 +++++ .../public/assets/images/sentry-logo.svg | 1 + .../images/sentry-pattern-transparent.png | Bin 0 -> 28158 bytes .../ember-embroider/public/robots.txt | 3 + .../ember-embroider/start-event-proxy.ts | 6 + .../ember-embroider/tests/errors.test.ts | 66 +++++ .../ember-embroider/tests/performance.test.ts | 279 ++++++++++++++++++ .../ember-embroider/tsconfig.app.json | 32 ++ .../ember-embroider/tsconfig.json | 11 + .../ember-embroider/tsconfig.node.json | 11 + .../types/ember-embroider/index.d.ts | 11 + .../ember-embroider/types/global.d.ts | 7 + .../ember-embroider/vendor/.gitkeep | 0 97 files changed, 2623 insertions(+), 43 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/.editorconfig create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/.ember-cli create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/.watchmanconfig create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/README.md create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/components/link.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-gc-list.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/components/slow-loading-list.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/components/test-section.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/config/environment.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/slow-loading-route/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/tracing.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/helpers/utils.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/index.html create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/initializers/deprecation.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/router.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/routes/replay.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/routes/slow-loading-route/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/styles/app.css create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/application.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-gc-list.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/slow-loading-list.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/components/test-section.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/index.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/replay.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/slow-loading-route/index.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/app/templates/tracing.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/config/ember-cli-update.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/config/environment.js create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/config/optional-features.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/config/targets.js create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/ember-cli-build.js create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/package.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-logo.svg create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/public/assets/images/sentry-pattern-transparent.png create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/public/robots.txt create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/start-event-proxy.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.node.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/types/ember-classic/index.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/types/global.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-classic/vendor/.gitkeep create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/.editorconfig create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/.ember-cli create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/.watchmanconfig create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/README.md create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/components/error-button.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/config/environment.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/index.html create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/router.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/slow-loading-route/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/routes/tracing.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/styles/app.css create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/application.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/index.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/index.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/slow-loading-route/loading.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/app/templates/tracing.hbs create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/config/environment.js create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/config/optional-features.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/config/targets.js create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/ember-cli-build.js create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/package.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-logo.svg create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/public/assets/images/sentry-pattern-transparent.png create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/public/robots.txt create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/start-event-proxy.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.node.json create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/types/ember-embroider/index.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/types/global.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/ember-embroider/vendor/.gitkeep diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3df961a6c778..bee434e40dc2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1015,6 +1015,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', diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b8cffb3698ea..f861f4b4ae3b 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -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/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/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 0000000000000000000000000000000000000000..1f7312b5f6af002e52a3f46c175df7d9efcce9ad GIT binary patch literal 28158 zcmc#(g;N|nu&1TyVIOw5Q=BWsDORkwJLPbP;&6D2d+|exyB=EXa4Rka4tIBVd42Dn zcr%&VncdBAlg(~2*(5?$`3p8C872}E61JSIlsXa;G8hTz?GQThKMql-6YW3Zud1ji z{f{!T@cw@_T}VLuzY(yysVi$Bu~1_Yy+_6+LuWvt#RpPgzhQpMghNNqNQ_EI^NtFG z0F9gkfQR)TB`J?9Ss)UU1B#r~XHCePt@+)QU{0*ibGRFN%45fTK!ib>H%SjoJS%Yr6Wmp)Pf~m<^$feKFzOXy%62fbsF}} z{z&Y<=)@U{h98l>WG-E^Svd{HwXX^LbFuklv?l^ihZPQ2QfYTCcav%GV@zm-$ZIgY zP=XYMMLB)8We8S6__r-+BWGC+#*6Fp!w2(CJ!8oQx z%O*X=;{b9p1~)Okkoi>19Fn>%%1e9?&O4Uan5caTY4W){IBRd&4e}=S;4sDO?&zKN z&8Whe`B`5)S?d}wJGBa3Dae0k(hJ3&a43sr>Yo;Mz^(kka($BovoaK}( z(JV%)3g*r9?F>1#35{3S!k+@W_qPc5y|k@!<_g{WLdcp1aO3e*iZ(0h^i5 zyg!JZOpkKlBpE+cxLUkwo_l!|`ydf3afmA^FxDOxtAvBqpJ$VPE-&fPUGva0$%~B{ zj$^ZCxiQo!E3RKw2b$o!)vyZz&=i?(A=D%1ef0+h*gLNH$hRg9mm4-LO@#%23fN8S zlf6+gO0%Cgz8Utv&CDqKIu`ecTqoqkJ5QYK2aiCM?j-L9emUqZJ{X2w`CrBiTPL9! zU2j7P%GJip)3d9&gfN5o#qLF3MYAxoG|XtE=Zz_P!o2d$9L0J$ zjORl+bNo%bFE?RrUfP0ImN)XwkodUZdYv$#v6pBWfTzfrjhm@}hjAZgN!q_uIIV}wa+W!SYFSddrvPpX3AvWF<=4l zEmjXve8M*7Nsmd0IH+}PB;M^6251T5~~OpSHbjx1j+9e9PsdTFFZC9OujXOsUJ zrO0=|D*rwHC;%gmgdlr=S<>yvLm+M%3w6Rn#h@aJO*WM;X`3m|Wb~bk?`VbfsE&xe z^NsO&7fpkJ{N4b@t*~Vw`}FTxx3*nM;b#FBn@?N$Id%E_Inh5g*%OvEHi9;Gr zu;rIk_0G>G-)$F~D3YG4h~T2uXZZE}D8h)}ov44<{qpVh{-8v5p&d#21$3gd64ZrJ zZ_bGCNMpIVpd~XSX)f5nl{dckmNg zE}gw*^8TW$>slj}Fi4nDe6gwS#dUR@@Xj5@pT59^kNqE7Hau6778GFxI=B@l1bwh~ zV%wd#l+hPbe!lOT zFVV0&JCal!_x&xj%RUxoKqNJyde?zp?Jr zk8cnLL;%Zr4w-+wLE3z2f&Uuph9Q;<9wQX5G@o`r>9myc z(Rr*=7-8SU!_ppsjpXk)djl~Qv5e?j~*`Xwn- z9QeB={m+x!^cOOP-hdu4rIp)Lp`=Z>?&42X8iwI_cvL`gZb1_qXj zSs;H34X)w(A)gq);6lP~)CB<0P&52FOymoOfBxED?o&sQok9^9 zf5Hg}gnc?xD>qAbb4r%cE#m#oXBN4MtT>1x;?unVDKaJ;a7mFy4|VLm%;Mb6=}d!griDYFm2PyP*8Lx)l8>k>H#?OyC;r}66aR8!hMt46X-BI z50Avq+J{EaG(o>@7GF%CH8RZzI^--z35@98wlY6O>?>c~(EROVQgRVpdJl9TT3;3g z&K`Rx3I5szR4l1&yZ9j&MY-fCPsEEpmi;)%KwXhq$E*@|OIo#>Mv=7qla<4c`orl9 zoT4^N(MkjkM1CeUkn0Zn{RKSd{ul*pJ)6#EQ4OZdZ?#mmS3$^zK=#D)$0KfZ39-3xPAVr+o{dULKC66SocJ4WQRFJ_kJH{#lCw9 zsrUVyWTlwV>E)X&C`BOp?P(GB#0Op=nM8u3uJwE1C4%CIKSz1$E&8<7!;yI+{9?*D zWNH{Y?`Y!DhPJy;Mr18?hte;H;id}YUS1iD&a!w(ZS@1C;gX zWBNub?*6v^8K%OQ4=djrW_gNks`CH}^$KpwJ~C=MR0t`4#ctc<-3dIe(833(Or8e7 zz%$9DjHrNboD+YRt3p#BkMJAK5QloZZuJ%4B|^sY%(Ai>Z=`l!~k0V+OUBd|kjT_qkNj2tHTecv!#t+jMI zelLNNgKc1Q&pE4J8><_x0s`6bdPK*o6lsD{e?pLMwSEqkK7X?TMhv0)HB;P^t&BMw zY_PhWkVi>;>wH|LcgKfQ#&@5;shvA3(dfxy7gLUr)4?QL;P4AM@}jiZo@Ha3alga! z#5O8uljz9pIZ!$mdb^W&Q#zrFi%g9$d+{A|1+wC(jA&jnqPzz4ySod9nY_O2bjil< zSu8}|yO3i~U9)lFX~lTO$9y_Z|7DY=kY@1MHFGF79fAW+sY!2F{d)YCZMp$sF~2HI z()C~SM21x!grxcLs3{RcoQ6O=;lxeM*fuiXt2voTtBO%+UgYia@5uIFYn6A+d7MvQ zU_OUYjKj>dq9|&yazD848hHcA7L;4k90Y{1@p++xOl+gG*C8RN;dlf{iV`LIuMNp) zMCWNYj36~%e?t7mpT+bOR5&A*1`|;>*S)@x2gn!_&w<@ zPPE1TJ6*;}PR+Tdop_%H^5Qe}l7D;~>Xkf(`TorvGGh3~tQ}KtgW1{`e3EQ%%v#l& zh8Bb$8gg@P{ykT6(fkYA?=D{yt*Gy^ODE)*L7%>C1kTSw18)@YwcJf8P+I9PXXiVbo@%&B!c$QWh`0;&lfs#Vh`bn?4}X_o zmAJxQSr_-^F>)~@3LY;8x&Fu0JslGCLj7Je-DJcE zqn)0rlSU2c3rXEx@b;DkpKGPs>BaBg4q{;eg1ALff;aa9MSWJiO&fuv)N64%t*3~h z?E1`hj3>H&yM;OFi6 zPIxrwfsB5d(aKu9kojD^FAp}USw>g%SocsR;|f`I{ZrmvyJLCy_uDi6%Ru{^udFky z5BrTLcD3z~;aOYBQ9nmhJT-Dup?aJYM)3$FKA!Jc{8KMXO`mRKVF4KDW{%z7?!sjK z6A^GH`AT05vx=m@hZh%PaR?75mHM~hjJigA_$EM+Tx_)sh;j80FJnfj$Hb*OHI%!Ud zdPUS;hvY%v(ihQ_WEypyn5?5z;iKtJ{v+EFob!fIC$d7>SI&RfL9X5@amBeU^abqk z%KgFIK((EgH`Hg*EzQTV?3a@oX?RREs&CKFLSQ3-pMSL$NTt&L4R&>W*3FoV<6Sfg$zdtwHk5(xw-o7v*L= z0>}CUsmJEj{W7Y&dHbVIU$YzR;3F{Im%7|;HQ^ZCvXd%-pf|d=OJQF_3w@TUNNaNHtx>67+#eJNJKc!*p-l<~Oa1|L41;x2iy zdoD)Z-z4Rhw@IPXqqOXNZ^=UA`s{^?w>=))75kr{{S@@$&8HA!ns?}^#X^0a!+`kJ zO5asFjE6l2vfvfnM4&6dZH~@cn$A!-nPIAkL4sX1)iD|orWeL+Xa&kDwgnjO4I|{l zTa2p+73qhpw;dUn+~^1|s;6F#v&ElNV;C8F1uyW+C$*=*%3JweqToAb(ZH4AcA&Cz zZ-9+!+@8M*KG)oEa8#b*sOzz-z?5)8RZ~vhZspA;;z9>15r>pV({n^3sDH%aB4=^9 z`}{-DvZ$X!xQUIeM54J7u^d(S&n(^HQm4F5}A56q-gH-=JhTjlW#NRREVyj1Z_iXdGM3S z2>6@mXd#-Gh&WflU`s3=zujGSp*-Js<5#G za1Ky$Hk@#LkWWaVh*W3v3XDwW>Crd@xmna4PcQ_=%7U-c#aw08 zG^-H8ILe(T9kI+r#2Kq!!1`yr-4!jMV3Y?nV83<%>vP%3(YkqTQiT zR4?97R_T1~BF~QimY^JeRUmi}dK(X2=e_Xx`$6MR)Q*sa@w;gwrEI)sDm*0xUdU5R z1pFQBW4^emkf(T=(hVts~>^UFrYkH z*_xj0MUQ;)hjs{ZZsOW#FNJ(eXvUx6bL^~upGg;a)`23E4!>#)mym6z)JcTcUMTt4 z`3-%bop6>wQd+NASxm2O6?%%ddP^TDGYkQ5?fg&;o_j3&I+!>R`Rx&KU4Q52XbCSh zfv{^@#w>3nwUV?3MGa*g{@+1f=LATrA%+!^ZUkOL12D~5XW1m`m(Tfdvd|=?^E>$d z+_~4^Gj9IslDH91)5*bT{&?U4AgIig1(p-3>lXxHND;ss$0r5p`JpL6P@vcc&%uVy zOL!B#!HKMA{D9jyASM#FI4k~ek#KY_+@HcQw#9>?=h9aT5?}p`v-UG@+deet;d8)h zoswuBvioJeT_1^H3xNk?%e!9qJ<(x_Sy&m$@*ln#;m2h3|NO zr*&RBy5VxD@KGU3XFC;|5rsNwJ7Y~j% zV^W7|DR8867|xQk&N2mfXTP&OlPL1O#L4y zcK~iEbq?@)L8E@D_Vq8+PQ87k@HwMEojH4CV+f=BTNsv;XP(Omv*E%i324SK`1M6a z7P20Arq5{?VHrm5Oal5&ans{6NP)3?dVx#nS&hSU$Ya_$yMo3%=tZ@K*~}v#L=4a<30Q z))*-~&J9)-NpEp(UKCGhG|9_`_EO4oajMWh&FWSAg>oLLT)~#D9as>oQ9Sflx~j>` zL%lRi15pIn)q9kn4AF|1;E|Il6K)Mg>WfGxW>5f=oc<;wB`|W7FHXc+$G7c1{90h3!fU9Sb(a;qtOTB-%nL2atGet#a#T?p zg16jg3d##Qjb$cXOmf@iByj$jlXI-*m6}be)Dh);cf)^?^?FWheMzcQ9uJXkJ*>_i2e-w@q(Z2Zv zFx#TLT%I!f`LzTKO8hW(jrBx3Btmz2V5~{0f>{8zYJ2BXrI}GRw%1a1?d-u^s! zuTkK=#!J~KGUprv4UQGQ0q@MWy#&mh@u5yzcb)Go86|!5Pol-Y_$eCUo82QHjIe2!nN|6XYxvUF{?**$=$}w(C%)snNmQ-os)TAB z`oRz(tOA{cC;Wc&a8P-=*)Odf{uw^olC!DH_TAUeX$jM6&ZrNXQW55RAgM#iD0Pwd zMbfz2uBVvl=SIl)cA+*9Q*&)=zaVxCRAftOu+5nu7zh^K-Td5NF@P2iVaB)&*p18m++$;+5Ypt z-{(7X2?MseEmO*yFxEqGX<)}H!s@=ZSEiR&teYz?l_dK~LFk?Bh55|b8Xc@TlhU!L z81ND1-fFVaPpKCVY7jXtAd}f`(r!Q`J?_HDX-)7$WE|F#xEE8gpg6_vjX|kz&&N9Uj* zTuP*4AsO+rOJP0#?|-ecf4UoKwI7AR(d7n{bn~P#2ZRc;|4K6#k{hS^|KhueEauJF z#&y|`{su5LnlKtH?=Vg>T#oC0o6WIvQK8awP-DI^+-*oQIC!B9GfvWQ;MW#tk=hxX zS!lgb2SI_r?Jj>9LCWZ-4GmvqXIzP;)Y_Ry>rqP}gr^Ya52^PTR(xb*diUu|%-BU#iNwYz#0QjX!>*vxbm? z8V)`t|HsjFO@BsqCA2ut$&j{xo$$e?FMCc~U(5*ZA}l=4^-Cq2o)+&e`PY-P-&=D% zG_P(A|8Gzcaii9|D^W+QC^&>!V9n~1U}WZv=1i-|Imc}u+?ZjDJg zw9^2KA;EN;>~zJuV)$YM=&hW7S74K*gs_p#JI8O(h*~H}-m1)tf4-wLbI=*w_yIbn zRMVVgoNGWEj85!eG?fqL)03FLV`A!FH+v74Z=A1XHKo5J_iBF@lrSL^#?#j>#TR4u z?_simyIRCRgVYmK3zHt|jw5x}%BQHz}Wv0naqtcU#2t^W$Pr-WX3A@TiXNa<&RMcp>Uu!7IaC#Do(^`DkJDTyMs zS5D$9R|}IOTsaG)qwDv{F+HeYG$-Y5?_d|?AR{L2G9tT}SRMRYH~y#`I84_+VMt2=AFCU_zQ4w3lq3WLO<(G7m#W8r??u8DL;vO>g<7})DJ;&bv zDttVR-c28S!CaWB7RAtJAT9^tYNh@)5Kx!yXxFPRt?FN*{3eSrs111kujU}1^3Rp& zT!%!8x9H97Ke64lYXgRNwX&)VzQj|E7|#jy`1^0gy?f0*d*InfHPRmF68KSNjBPd< z^5HqGhW&d|&%R%CDYDNCs5{DBTZxO=rKwekZT){0 zj&ssyicxg`^>780T}RO01JuZ=SJcXTJUo#JxnLb~B=c3eOKF_)=VUG04+3^smRCX*P3%QD0k5Ek6s*GiI2d4|e)xXh!7V<(E~obfHf= z$+f7umMJGw#95gJc9iqiLu@EZLLLq8Yex6Ow$Hom&Bu|GR3ZOgx#Ba4_jGXE7u$<-dTr?Ri1)1sciVi9}PmoL|+%Du{1V<0jDO#PV)>qjw zd*6qaX7LJ3$+X~r#Lhn~VSp?bnet(A8y@P@!=is~)Z*H_x8A(|Tp>|u6CE!F%XYpb zR&}v$gm`m-RV5CK4_3Qdyey~GpOPcTC_)<^*d|4}8PEflN+qM{Exx+i~HC&S}6U@*!<&+K+4%p78f@<$LxxI6oMj$Ex2q zlW^N;mc81*X7@1DeE{)edBr>eK=&yax@{M+xJh(vK$OU?9Ho?g-n#vSbB^n?+dS74 zD`^s_RKP4>oB6Q~nt2=0+16SU^9OZ*rdKrrx{JtUKxkzn1)@p4+-N&bS-5s2=2>gQ z5B|D3JT`1H^3WJDc+(oHS15fPh?8!=+t0JWDevbB_2*_bInwNnsDClY>A`#Y&A2P7myO*ktYI?*6+%bQr>{jXdbLsPTQd>T zR`NUZlLIHj()a)8E*M1GL-nHxyU=(;LDZSSL3ki|2D?1e6MNcF>#POB?|_dg)%O#jG)VU@@nWWxOf?)h zB=8D!Ye;D&Y=K^(z=9v%$n$v!VNrzFuDPonSU8UT4(Nh<{z2~oFwUT~MC(u{kj!xm zF&m!INhD14Be+9gfjZzfY5Ph1z&myP@Zi~&L~K>HmI>yf`dKxlDT>H4t*%2w^r`(K zbP?u?HE96$`CwdO668k~anukUY7(eS#zWuE_l@6Tbb%edNxYtISejKA8xg$ab6?!- zCrog2L3%DKPb{qHI4Gx93yqnZ%@FbsPkv_=%?c@OgWuumR~EBE1W9BSJE3nhb@bws zV|4H0JisjvT?ObdOfAH4Yx*HUu)pse>!bpvM|(vtky3rW=d7I=xna|fzfs0$b-(Nk z&givJB0>loe2TJ`gm|%G7b6d zvk+ge%f^*Kns`|Ap8Df;)_|SmD?HO!0|FYI{c~&b89vtrzk)(F&rEj#`w5?;b)?!Y za{!Ibd2^c<*%FZ9ed{H^)o$>j$L+yc-W$yGyxhvQ=Nc=WYSCo(s0IZqprv2eE1X9A*(pX%~c9fA4I6E6wtT2dS zcXzgMyf4x20mBB>HikeZYw5(m(oYZwH?ComYrb^bDtvx(`#b1YK$u1YV7+f8y!&se z`k6iw?mxdcJ0k0#4XBjl-b9|CeZt+nfMI~h;CM{7`>B+$ZfMI8vh@?YM$(f;sF~FE zhB0}xbix{=5fJ>5n{1c?D}sAd>n;+3Z4+gxK0ils$qOok-K^GF2Cq@!EQrz=-Tp(9 z3LDcd@^}bDz%L_O*cw0DaTT%#4b#Fo6xKe(*g?Z@4}VgWs$S7&1p z+v%+i7R47ckAaBk3yQSm`hl-z*M>ddOiKHpnmx3mkS@Goa@%FdB&8YIRM{16I0|e& z_Ro%P(=-8pE|y{MOwMK0XL5-|i4`y!SjYwu!=tKZ<=AC+&>dGTv0mZe?*-)i#KI)a*%{8z?8#c}cx){a;6Ihl6EnTO;l@p{zd2W$t~xa% zHDJ$|zL43$by5Uh(rP4@{L1U=!mU_1#pDWg+;cHI1u~aQ#7~2Bz|r3ewv!1YXOY2Jbs+n5ioNTchM@&0j}04IUBisf9)KR*%P*%o zh>d_&V!BJICg?o!7a=I~pYMD*|63B}6XSH`hY`cbs^aute_N(JXWY8SSuy^;q_7-H?+;#b1=JgU12q6il;BJTo}8!IXPN?wr7VlDdzS*4BN^9?s_ zY}-L<%B1DVZBoc8%$5&+U$8J-;Ddi;y)K-6;)}KAsF6G0f5A~*eEfPdS38=%>*)a3 z)MgI>T3~>34W{;Ik&A-#;GH-c0}tkD3(D(r0yNrYiq}tli!|$cVy<77;2FNNPx{Au zPY|^f!r9Spa>sHTu2W04J83AU!YD`RBdp` zUPfYXXAoKzFV$%4z+C~6`n#gPR>fYt8ur-DyT!1qVtOxbHKVK60SY6k!$|TJy-O63 ze&VTP#rvP?7^>n2V@NK=^>e$H4i8v2kC=WzAN$K5pd0E&JU!uQuojw3!FY%T*T!*Ebp zA=`QhAh^u}7?)vSP#Uz8DD8=Q9o8br031jvMM>I^$O!6?Ywmk&cS!qE1HT12H9h{S zr)oL08U@0c6&@`3N7vRuY%MX9rZXqW#Ty0`6pVeX{5-BgfDmwn2ygvU4;Y1Tj%s*;#1sFArg3s7zHTMxEc4Z|a_Vk4jyprT@kK#jDw7 z45o&Z!}^^V*qe1uO68jc!;V_f<_q8`z&b9ha6g)czU!5BAV~ zEUPkEK#_;b8vcBns|z(0yS1wT&A$-d(W5>9IlKwsU^)Ztdex5;yk?mf_|(@(puOEr zfMk(QSaI;HYzS3_HR-8y4csz7J{s}%vFI9H$E`rcvEUrr8x+qV>a0Z`uNOBI1l}tFn97>G25<;L7XNDu<@-glupZ$`G3Seb z!(;aYBW$08A(aP9?!6?6tB_W&bvPI1d6~r`K=V7xIX-^a7DoGfLR;D=q>|V(B}~$P zreHX;tf9%`!w8AA_T>j0Ws6_SjsDXNKc^Meq|E|WhsbCW`>mspYR{yrQ+{O43_}C^ zMzeqAKRcAgoijFUj(jr*cPtTc;L%Rr^aVqC2cZXNUcm11?&WAx^wHy@&?=R|AO&5XWV5*WWXUD1$ zULBIa&0BB+*utjrEPX56);#8|8_doUODrD=EJGAWuJ1RqRrbDx{MT}ZcAZ+NgQs!H z$YYEVFKKhSc6wE|p>SaSJLTn6T8F~UTJL=DdL{S?|&bSBGfKro097%66y$Q$7On#T8eMs1t2Uv15-dA#Del%&d@zEV{&8wtZpZ;8dFa%ZA^5 zBofGruu`fde^1w;A+_rms@HJJ*x$iaGwG6Lm&int$s1hAJi4SSZ z)@FVdAIi0h4zN{5`KD>3thOc}{nsa+ej+#6CQlJMQ51F7Q5sdY_eubQBU(h?&7=>z z#UL!&3q))jJx$e~zaKv;4J}+*LY-{jOr-QSlQuFGhw6kI?TDp6B;SZZo*#k3@-i*8 zzV5qSBatW#UP#})b$1_Q-MghmUq0vXEnZFfW1cxieN@Lsby?>-uiafQGb>>0(Xm1q z0v%6CW)Fsq_vB;QWS&t{T*VTvpSmVS`bXtC7l+pKN9YK9lFY>hRh{FmrJVfimha6h z1XQxBW&AXUMt>#G)x~{%7%A^xp#f7GJ{GK}M#8A<+yMJ32NTN!y`?Hfd;6i0FM2gp zvF-13CJ)4>urJ(QA=|rSYCL!M-A9`2?{T*@)7Sq~GqUk$@_Xr_yq~o3j>xc*uJ>LF zsdLvgbHoAZ>&XUjQ5pd@8UMCA@`@y*1{7H*(c>)x57)P&2d~^>rdHmAH*3~*0V?f! zGT~jt^^Lb&@iH>r-r($>lq8WzZx^mLTOFk^Xw1ij`SXughHA`3Tjhoq#Mvqv5_!tH zlgk^R0ulBMH{!|baGmw7p5zKheD=`%dEPGX#orLd@9%XQ|3>lW)Hm-D>~EA8b)EjC zc~F*rFAd=kh<=`t8zL#{W_yZh)2dSnm}$On?%Dt3G`~a8XnmMF3Y;+8P;A&Va+Hlu zFvEP2zKby!!KcM~V_n=gJa?{&WVAkFk)cVkir)TyIFk^-l5lKf8WxZkZfzvDf5!E^ zHvOokxv5q7livxq|D;EU+r|Zb&9eWn`!AopG#XqfSK@WLM-Q~f{}avwEOI%qcG%2% znlW6kmo9rRedDzq0u6bK9=14X4!M6#i3?kKDa&#P-$PI#I5 zRavsG^4`?4D38?7yRUdd|=MfSdhw6<>;SiVP;=0nBq^RU|1^OaRg zU_*5eV-q1pftXe6S(rffC{}w~koRy+oK|*!IYsm5VBpNT>1jjXwSS*dB*69a=4g7$#pkSbtPw5r(mPH;ygAO~D@x%yAro;{OfCCj$QVtO<#C zX%#CiH#rcnz7h@tq#sgZonUzkHfHtR5w%>qUHIF-?=P>(3s3FCYPRzMXJ?Pt;|~6X z_i5baeTL(4h9VbM3fj2*jn0y+FhV%MSQmP03OtTIB;db~mAAAkS@P|lH0S;d0cK2SpTSZ5k}PveXgf`SRy)F!A$7$9l5r$Nl}r>A@=;HQgi zY)*B<{zK}Uv3Fh0cse=Z(noW*9Y^e$n&$Q!>}$QTUa#YdI$AA4QQ0UG=z^^W{a%9L z)F;iR;pG5pMj*zLzq!WEa5eW|*UKDcO+z&EA%vUj+D$t+t4g%MPNcQ7?ugvhhSW@H zi*6+J5|!fnp4OH~>zlQ_=iM+jUV{HdBo&K*^FES0Hg`#KtnwP^U5!gQpRt;>Hf8^! zDJ{bSn6>;gm4}#Q0c~-}&PpXQmnaH``g66hGMP$egq=;smFJUAV~H{z=0fz)3Ycw`c?clEjSzk zBa{kXJtLJRLH$SX83M*K39jx4d*^H?*n`$Jru5|xEz*Ce&GF`&v?-j@%vMSjZzWz) zTEw?65o&alp9g&MTB#+`7$H{~^=kBU{t;zOlvL75p_c)xMFpbja)io(#+edN+>nuWQ*#>C*Dg0dKOu0dlmfB>O@Bgsa>xdlOY9|~gdaJUY z+$-h4gGy7ckeV#`G>d?Y4=A6|D0AO`lQyDYk2i^VT(2z4{u0KpG;v*aYUUT}|#Aa_{@zp32YJ@%gW# zqB5hZ*F>jo_7Ft*J4|!2JgCkGN$iEXu`*zlnun2F+t5=a7*+mVOobE^pFSIZM{DiU zHJw0`+Em?`hradHm6Z4Op0uoa=Ebor51IKzl}_4frq6%2=9#9J`C10z4MCessH@it zIF7Q_cr;exv-6#pLk{RaRzMO z`l1UJl=6rlg?qQzyHqnsy*AkJzb1g`h9tAzF(Xx7NxdE+%J|98B|^b*g7J&UzBL9cK_ zOfeWV6yr?*81yxkAg<@!8~i}X=`i|Cb-f|}^=60c1T!YDv0wSPHd|%H;Ln;^6)>|8 z44=n2A~EK13VM9Dz(H&JAT^9m+hYZktSDRfq3RW4uSHPdl$6Q= z?^f39NS^Ty)Y@!B6p@cvUcTcg*?P|R6`N5L(s_Y2hCBC1$ttetMXgeubo&F+eQc3z z@QAvH%>X(Me^UGu^IXXalS6ZS?_~%hLaK~_dGR{g9pstr4&v)Ph~Tn)1sF=&P;KfN^p6eU*^H zshS{ul=1meP#`gXt6w#kq3vizXTE_?XnmBc_gkQ2GbU?XWuY^;QC+8Qh^i{K3M{|C zzQ%`7B-%83tKm8|uZC28`dbm|9V1h@7`EUm(?`d0VApDb&5$aCIi?NxxK` z)Cd7UOU+GKZ3f5t*A9emZu;3EsdzX1O!RW;`8v&KcdFJREF+doKzQ96&bamNdV-U! zkg)R0Gmwr74N_;KtV|u*W4ePw&9Z`CX<*t zj)hxuv%#5vG;}j$Y)U?Gl+T45t1ffSd_TB4wu>jfp>ZziCk>L9Id#u1PaRcgfLc2@ zsipch8{m&r;?CaH3DRlKY&xTPl2U!U+LBx3+fEw_s1ldyF--D5FarF`aAz{#W82)} z{r#gjDCeZAks+8Ni7NkS^EqlrG{8pix6<9mar=Djud(efftb3DoF}W0J{g1lU@Ahq z&%*gd7mGXPchCItC-(m^o*s4z0q<+IhrbMcJ>3J#lGd!Pl?jr7ZpjyrKG_jTLUIdSe0Q&7hSq;fv>Heb{Z!0X~AYU=4jC zoE`8>EEHU&3@@m!2T97To7cXeN$aMGNRks@Kw3HT(h5wdH8p)v+PaZ5^_}{qkN2BL z1|HjWmdJStom@Hp&h6tN9-EhvUV=~C(-u4}5BqHYWo1`fTK3V}JTDkFmb(zrrd9XwOo6nsChBd8$RU7HQ~1Am?bYOE z)%~nfX~m)^|33jQEzr`YI9-YqTon4NWO`vzPb?<19@rG7rD8HBKv{TbZSYF$TTtq- z3JuR=E@JBx%BhW=Lf66bi>5@~NkC#TQAAT(U_=LmL2Bn(7;OnvE;rdUd?|VN)rrD_ zb1j_2{Q~XMN$wT92$7wUG_s>NCtnvVVdVu%888ebZ>=jWi_FFjvUo#G+aKLx(h%|f z+o~DUr3L5u(S~LR*Gs%^7a=Yi6S-yV*wKr?!tMud+ti-LSv(@^geoOPBM=nTWbwNj zKq|Wj7I4>8uG>p@?C5&&H|-(>I{^4XfuhaEzRFluLjc0@;tDLuO2|^CQpM8PPYFtg+*+M`ne%El$<;XI8G@K98vJ5K+&h_TgR&)34Yq!^PhB!I(vRMzBsHd})`@BY#e>>Z2mi6q?3GScBW;1#7OV z2exx4R);|PNCoR~(fyLM>T!{;BLAXZsj^^Rs$$AgsOPsYq61K##)!Koz&i|{og7rt zt6lJ|qV>S^{p8dmQ%upHIH7x4K)m+{;FgZgWF zjWuhDmak;pad@(+hWT6g8?Txj*{_gq$+CJVWXI2oyG9dC6>@zrElzsbxAfxgj}6g4 zMtEg|zR4_9{yKC}$PVUL5af?5rU=I&8x@}zs_dmpgZu5%P@%zOTi%{Oe>r%G%1Kvt zuJUseFr4INmHitdMs_g00y3)}wb1h>%a8T&71gH_KDA5bWJ&zHpyjKt*40sMtI4z1_+}z5d~$dVl8i`u+hSC{;~mSbh==(;@A8qgl9R z%@A_lo8M|FM?b&ANp0B_6jBQI8OE%lXP|fC6AW#7xcE!K+hAY5Oj1>qVG!&Uu#{*- zbD*7IG*U3{j;IwjF5~lyDkCly<8wF8b1mkR=?qTsDiY7Y#-BU@1 z#m)!Rir#>8_{H09J_9u_*Ba$=dBjk<_Jf^+;_Nk>&$is-Um20(P#2@Eqe<6$IT2Yt z)a?|0$)=lQ%3Br8vbs88`6Kv5k3%)PGb8&9mag-UP9SQn_JX~GniobTYTozze#NGn z79-rybGvTmaL@hHpKaPI7hMMAX9p|t^DFv-u8&V+=JoZ0Hu@aTuk_~2=d1-@!Hprg zWrbFN87}e9Hf_aa&J6>A^Xy}U53lHB`eI|5aZzAz+3~RNcBVkrvZaL`;C%+mwkMWO z+7^O+5U(nlxK?c^XJ0_U6~&H=e)|m+jT2`+Ku%;>=o%OyC-_gL4R83bk2ReBpj?%9 z)sGIx!#A|S_h0|GQTe`JZWlMpI-7tWt>OK04?ay3evNE9L{eXG+a$_*A2E2virEO? zrhAkohi8Lu{qdbKlosY+4PSXx(;$Us%@LyL&Og! z#pU%6oDpJ;P1AWFfLr!GY@SRyAdzNX^^LTDneZ2*DG(RD5I?n zfRc5Nqu0n~9~>S7RJmRqD_-XFMT5FhFIS)1NbE5k*$0OQLsZ=l{YMk?9m$8I+tu}| zKQyW@D|b3xhzJ1Y{UNFAMNyqjkr4>fOL{wtUzlG)ozjh$D`Sr9sy+ck$@d#uYoz7Shfcz=8FsZi#Q>WwJ z-$gr|Vh2;t33jTd7X0nN$IsCus?(|Hgc{#HYgn8iyeq*voi`9~o;S}=(mcZSynLs= zON&GH3h6AZk32k$V4cpLQC`3|R)Zq@d1OWS>U2hU*)I(z>MNF@*Bo}?Ej5dj5?rMx73Z7Y?EF;dE1Z+ox8Y7Jk8iY@MjiH`Ne5 zG1zJ2my&D+!Ou967gbr(P6x>wr*U1b56`=VanKGm=r=2)R1_%gLKvNj0y;6w9U>h2r$9m0qv^r%fd6nCtitTos)tC z95`2yp~g>7ds>q=Dcz7yyb{GaYxD)wgkM;aYl~v9B5R`;s(yTVM?VtG_%EEU zZe%NxdNavxABxWLQEIlI{@T$2?xK=E`fkSY{WUBhgt-iwN3p~{v+&U~8y%ZmeM-Bf zUUD}}#QeQdOQEO)y3_H|giI|i#as9LU)0O6*YIXPt=c;1U=Ta$vMr^QT8Q^E{Ysxb zdW{BlWLHQ3Nb?l0s14$;=#{Q>FUuZ-LX2`yutU50_K^51O{n58=_Tsd+B#naX=&ys zc9HR4(u;9;4>z(s(9yrLn@YijpVG@JcmpMo?ShW^6}{lfxQSlcfwL6yBL@ICjHN;p zH`eP_<5$o}mrDpCZ^T2l(^tQtm%Ztgw$Mvw-7U0&E8G@>m1?LLL!(|%Ylwc^wbkY?!>7G_yA7_3Bj)|^HIZTTbpZ_Pw6)mSeM zQ*Cgvc#Up4t~T&HdJPXl2RCxcz`0wGWqgqTCvkq?tH7lk|0albSJXZIU=DJT;t7L^b}zB}3rNn2LN zYU4iEY#*h9D@wl?_{K|SHNXQ5vSZ+3s+Vl3SGn-cT=n73ii7&))X%Cy!N^@BfHi?)h!w zzL;Ma4Ll3iShusionD?pj(>7EIRtoz$PU49)k{&HriET)%~UVag}y>{{fqoZ=U&75 z%4u93Wv#&ldzz@W)oWBR!#p4H-hs12pe}mxfmRus=;g20Jkkrp^|SaDy&~i&yl#3e z4CIE{>N>{j7ii!SPBchpiVcr`dgHzNIlTm2^opA9IW6=eYi4?lGrjn2B>f|LA>_y` zq6KN^%YuOvZ~!iOy05pK+@pde$m+>&27HX9ZuO=i#b<!%%7285UgkD@j$7#k%#ttH2MnYa;SXOF zWy%ROABVi1m%lEr2Re*RmNlIb8g1Yn8{2h}Kt8FLxE1u8-+ z8@)!Dldb_$SsDoG!ltb710}tqDvMAEV%OoJP|K#}BqQgXH_*b>Te@Z!NA{9lc(;6c zm=1yUagWh5SH1AI9;#Yp4#gjr1uk)OPDwEiw~rSKk2}0x%&cU-y&e?YQ2+jXZM@@4 zWp_^hEf3e$u0_ZgJgX-xN18)*Xrv+{3gVm{4|mZkOvZDfUa!KCzqlr3OON{rz4U|3 zw@Be}GEh=t|AMVdO8e$=F*6?-)?Y|8^l{le;iyLuGf0!yAve?uLtZy?SiS1EQ{+L6 z($JT7m6r5M%Zqua2{lbC(PhCdR=s&JP2-_Cu3qt~mzZ1)org0MbNiSY}H7aIL z$BF&jW7&`mJ@nR%*q+?6LwfyU>qx3&U)PHl!cVHNDB-LDyci3IS3|6%+;TQ?4SB>h zqWB)C-0N{~=ja2A*~>D=?{oNls3!dFVq9va7orBFw!qh99;uXKG zFn?)rXO%nSVzs90RZp5;Wj+R2Z26+#g&Y6`{Ut%KUlxC_sMX7`T10thC>PEN%HrsG zT6Kged^z=^M+dm#W@AIM*ApLTovTH)So5xQ@wj?XZe!tA^oyIl{C81CI@)HpFyr z!^y2Icrkr@(~;jk6jL5ZsH(eIt6RJ+ET9!a3Wh>_LbxCUWa!1zi@rAWgM4i0ii>4m zrch&8TNjmJ3miKlbi2_sqt#e`ki0yEPd^rZ{TVM0=~b;>?Mit?@w=Yv%COl4aQ44` zG4M=CkVMNYG5s`oYd22lt>zG@kyB_lg3=5`wgP~=uAcri9r#s+|R4S({^sMFTz z6`D0m<;8shlI+Tfg@Zgq%znE7sVQr*=5;4Pbx1sqz6axP8P?4X~ z3cHi5>!O5%wy4OC3qBqQjJMnEkgGIazol3KQxSedqY+dpjNQ|`zLzv|X}Z|VbGtAb z)`^*-H`jc&AU@FJOVC!uGSOVnAolKHqr-o8jvp%HSO^zP16airH4H8Y*~sQgyYTtSq_5#!xY zF4l}#6|1FiorIZPsHWfW6rn5-kpfZ3EsfJ(;%T;*a-H^i)}EhCL!-THPIKAI?lgIa z{d(}C$A(3GQ1{0juBPU2XqJ{Q6ft{t!H`|#kk2=r@rkObbYQu>Jxpgpn>|n~DI;1>5_@tZJ`7%YSrm2ISPftC-9jDP?&ho?*?vtBH9K1Ac}Zldd8C3xj!QIRgk`-C_A@~YXmM8FNFm+CopuXq&ay{g~> zC-wd@QhnY$dC%bwpZ!9&yCVkldZ};Mt5Nb$`GqP!3h0E`f#^)Mk|Id~C5A`J{5 z*+mnOU|)?mf>lCMhzJ^2^x~cjzhU!JDGCMll+d05s6t$?cpS;$tc!}FkmW%09RV%EaU+Cjv+3qT$ zY{{tx1m^r?UV08ZLXrLgo&P^(vJ~DhIY8T{=^3$l zk%_>6qsyRQ8Vxv5Jb9Xx#a&dJMpclAE!v;mJJGGvI>fj^HcV6cQ4Rs7D4w+a?6RS} z+w;;{Iy$EDrhB`CS+@-^0i!DDr4r5(p-!+)BTz({_&<`A!m>>ee|YvmvNOgNED1>S zP{W#lG|Bt6UMOKoxQ&U~G`EX|Bb5eZoz5#A0wgRjIMKgxDZudcYE$7#S&Pv)U_wMG z<76?_LXH|)r}F?c(F0-xx`top$9L+3qwx7u{1 z7poqQR_Pxc)LV7X7IfRiP?vP2qxB$pLY8qVPvc=Ssz+&8ODV-|zx=!U0yaR^sfWbSlEH6NHQW-lUf zp0sCov7&$--XzY~ z0_4~9k}BUe>Yodza`HlK`1&F<#sIGoBC^hA#O0=o_+**^h2#oqkn@dI`a9L81vB(> z_4^`kO{~7V$Q%{Cj?%2N`PNgt$l|S6ZkYg25iZ$`1)OtA0>BmVm1$FD{E(E`_1Z9(h zyYLi6?Tz=VcB!~{M)I9rZKL}9>3UK@4UTo%!wa1|hkFACf8VV@)%;a@g>RlP#QhsR zLW@T^p!rZ#u;PMtP>E)n(Sw(JI1NG_pbVD+TP+Uo?+uTYE-x3q*C$)34|Jd(i&F6y zl*Tb^cA{EmSl+wPm4cU-3QD*%V)HF99|g;4zVZ1fzb+0V?e>E%0R>civq>}SGzJU7 zD;0v5H|teRN7gG}w|aT%MT0|G{`SV(Uh50?b=Pa_NxfdC@X~{|%0c4>VRZPGUb37I zRQU8o?mr&Actz+UMAvKUX~(jWqr(|yUmGBEEGJvcb0uX8%Ntw7=k#*A=71cBUTWV1;VUuk{4H{>-`;uW!$XqwAT&tyg@I>{<+!Qu%D6)}*iL<;ewK%*%0n z+>tMpH?}z3S})q$R+r6(pjl^=#IBlG)hI09B9%n838c3CZqm5acn03d$9=CG9bb`e>I)kEQgio(X0-RU(v-rd@q zJ~;FSigqzha&67G_ReNbNGvZb4N)-W_&>tktFo}MWoLSE_^@9+wGre1xm|?*?FP^5 zTEr)ga8P4;V+*of>NWJj`A-pmnAt^yQ}ph$yf~;6?vY;fU`*vf<}v18%~!!Y2^NH< zNy4%-5LGE4pxk*2;e3exoPG_0b(qL!P!+SbgXtxj6iI4zr|27XINcD5dNc&-S*MWydpqZNG`+?a zrxyw+*Z81RKnQjwtPqUoh-{Jh%@wo5>BU>jy78e1rju6=RtzM+pjTbjHZIz9_aARhPUah#MIyv&LUJ2Qu^xD2~Ui^hG3^XV-H@{HzykVz2mLk1q zA^F$zV!E;}kY982!__YGYqh3Elzwxk;|J1< zHY)$1UbafFYuF0%X4fdkn_tP1bDhroXilbO;)A5)0)J1h0WM&5(_`~D0}D89HIA%A zr(+rMn|h(C?-&&)xkrLpnLOFC>s3YP_!aX3+1ubjQyX50css%SZoLeu# zY4$!=Q4+m^;V+t!UuX&&QhMg_7)St>eAlbGErBjpL)CAm^o8w|U9Y2HAFZZnWly6Q z1pID~`8Z7tB0WEj(-cBv`^5J~1?7&IeC?+EvwjV*z-+5meJdNg=hT7b23aS^iXp5H zc^vyqG%OLduGSvb?UqZ-3VS2ABZFymHDhEyih%{8vSkn1QQt;XtHPN>W|Q2Z=6n#W z(;R$w`DymESow#8`2j+azkq@(sabA7a3J`mO|U)R=wPExzz23Eb^<7pMf&<3d=tG$ zuEY`vwvk?=qBXltThv{@%&7{Ta#ta+YfqB0HBQ6GLOjVntPpqtmW}}vV7i=+h_tBSf@1+GL+8k!aepQ z<(9QaR_j&&ZjJX2e@zOAaijDOXPwq~m|-Rxa+Tj_7G3E*>NQ}ySLr0ODM-l|%^H%R z>-E{i)p%Wj+0}xwgdDv)y|`%8P>XXuM9Y@gIB?FBZ-3M3dTobSFvBwDZ=(=zIOh^~ zw*o9aqL-6TGWz3x_MCOvV-@7^zY-t1%+$EETL38hgkJniMa_YAHURYi8RB|%O{0e0 z#bw~QgDa-%)p|_8wxc(xCipd<#8B;G*fcaayr-Ak!{E@6-?LGT?0Qu&(u~V~I|I|Rh|)+}TL!($@_vz0u7}_fy$(!w1?T7N6|tGwvPhYg zSf0|xG5j*79PhZI8QNIsXi?`H$&9jX1LJFjhlxKZe>FHi@zPmsXUX3+Vtd-u880W8 zW!mE9g8Wi>1Rshoe`n%VpQ>TEUvIsRFYTE-<0U>BFZ2P^+60I-D1R|HKk;&B-CvZ0 z%(IEt?WKB)(%idmd<7TImM31ixT*H7oIVpT;gl@Prcr|Ow*}`XUQ+r(duATIbYDdR zukMF@_9?vKv`(xi%0y4e6R!%{B44rZZUFI$R(4kIidU3`LbQLPr;F?yx+Ru>*MsvD zuPlpv8C*AZ#cPq>WqRV3B%z4qH2dP^h~UN5v*n4G`C?bS_B@OMytM4EwG&=>KT^u_ zRf;MiW7vZ7cL(PuUN+GynD7(ulAZ7}vLOBXU8Owhi8B2oPE=?v4o|IxnzF~lt9jm` z`n$!kO6|t$#A@@pIXFK<8$`>g`-8<3FD*%V!KjFgzf1U?c-4iA0&w)|!BQ-EDbq!r z@uHW9i=f3Hly74ELZJ4K!wbfvg}PW4Mq0yxml0jm8!r*~C$DO5fWmm&5aS-Zq59Ck zhB=dJhi+j~o}3K_0~j5hpLn&*>otSfyxN^DBdF5u z`_eIj#!*Xre~clz!Vhf|uRG-tFgKvhtNuqTlsjxcS>;y9+D(N`zy?3n6|bPXhy|}i z)Xrg2O`%Y8*I*m@x1XKTN)H%Ma_Q&x8JKSt-q^%gHm?rB`JLZM+U5NXPlXO$X$_es zUbo5dnY=1&UWAU@HjAM{9RxP{*T*WAnb?5CiPxR6Lum7ngYpfGUpsM^>xU>}o*`5^ zYQ^SoLT{KV zM}-ab)$$ORdf_F~X1dBZEe7Ci!6D6x7nr#2`<s@R2wJQwzQ)j$rvEy~X zJW98OuQGm3m059s7r7kJJa{>ue+>yXZ3D&uvJ@oR=QzB*#$3d{#dYsJKaa+>bDJ0ao-c1 zo{BV$5bL?u_7I6sSuJ=di$6jMGgp5}sjMI~i3Klc*p1!hRgYKc&}>SP4VQ3Upt`y0 zh!@UGA^!S8c$s$0cktTJaG`q&A_i8xH2gPyn-_ss2jf?F=>@h9_sY;9DQr&OoI_X| z+Oz+~RU+zd@Ir?`aN>oy$d8xfw|Uj$g$awpDVm%MNhc{coh%9`hH*uL*fZuGb;Foj z0gE?yVMo}=0^t03u?6L=yMWio#xJK}JcVx=+jy%}3aUado7L|H+bT`It2>R?s?_OO z#4NB(*KlzRctOi5ew$Z4UPBwdoGn(!ITk4!gPcI=mHtpj1S*YtykW@ZZWxa#;Y)AI{5k~eJ{4!LAs$S)(;JR%6 zj*6K!@!(}jt!FCn68w14kIifIs>h4?DLpz~F33}=Tb0=OZ4!gv!D}zQc>f`It^78x zdc1}v?tFn%>X_?*>QY&=rQ?_&#!2iV(uNT42MA-R5;2uQ82Z<}tF7GJ5=a*?r!x z590#Y!*s@LrQcu=Zfhe^!Te9ZUpM^1Qoz)+;l*n6x{epLzZvxYA7|vL<>;Bz`L1O) zIQ;I8*G?`=-9ie!Tc?UiEm5ZT#BoFP!3n!FYzm5xHtXC@KjvTz9-8 zd9QQ8;r9(*kp2|QMgBe?ugV%v`}Q@O_3Bu&86qNEk@WKlWm$2#W z0eXd_EGFSkls z*5N4ouHcQA1#RL3@M6B(B=HC5OOFfkJ+7!;JRKY?$dI>nrapL;3e}*&QWr!*?WO~`|>UFE=W*F;z!|CSK3KeS-DpJ;5WA94j=oW?IWcUskMyiO1KbVTS-!xz;6%YJwApyi`7?7$(A-ViWdhKojV;`Ny}FHoqVh2Qv9$Sc6l3rPsp zq&u5>Tgs7cGG2deBRKKmSEzRt`>cY#aM@*F0XApU6MK4864hu~=9f1IZd%cC#);Q} z;@=wa8^3lsP^jd0rF1*Mk^p%nK?}k)@fuicSNR{j!TH@%>7+(}UqQ42Ow-@MgWQSN zh~ht%;x~T5*-}vJ`K*U5NF9L=+}|gyUMmGV4!9;>sPWfrqZ_|;JYN$b>#1X^EJPE) zO!=7W5MtsrzWAdKgBrgirQMNpiE4m(R?5FWLuSOwn?_3YcukwvXvVLoQI5=p1gR)a z9qQiMtKtk<3&D?9md$k3;5BVt6=&Cc0Ym-n@_Xxx{a*KkD+v@2US777y@uBe%JUn) z-hHj_@JbSu0izD`MYvL8$1BQ~QXh^N { + 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 0000000000000000000000000000000000000000..1f7312b5f6af002e52a3f46c175df7d9efcce9ad GIT binary patch literal 28158 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
;{OfCCj$QVtO<#C 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 { + 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 From 8c0631a59da1eadcb97b8ce8baab74bd7f7ec7ab Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Jul 2024 13:46:49 +0200 Subject: [PATCH 06/17] test(e2e): Do not require DSN to run E2E tests (#12743) First, this means it is easier to run this locally. Secondly, we do not even need to send data to Sentry anymore, so we can adjust the proxy server to stop doing this. Lastly, this means that we can also safely run E2E tests for external contributors. Closes https://github.com/getsentry/sentry-javascript/issues/11910 --- .github/workflows/build.yml | 38 ++++++++++--------- .github/workflows/canary.yml | 12 +++--- dev-packages/e2e-tests/README.md | 3 +- dev-packages/e2e-tests/lib/validate.ts | 33 ---------------- dev-packages/e2e-tests/prepare.ts | 5 --- dev-packages/e2e-tests/run.ts | 23 +++++++---- .../start-event-proxy.mjs | 1 - .../create-remix-app-legacy/package.json | 6 ++- .../create-remix-app/package.json | 6 ++- .../test-applications/react-19/src/index.tsx | 4 +- .../test-utils/src/event-proxy-server.ts | 12 ++++-- 11 files changed, 62 insertions(+), 81 deletions(-) delete mode 100644 dev-packages/e2e-tests/lib/validate.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bee434e40dc2..779e221b093c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -971,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: @@ -1072,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: @@ -1109,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 @@ -1149,8 +1146,6 @@ jobs: strategy: fail-fast: false matrix: - is_dependabot: - - ${{ github.actor == 'dependabot[bot]' }} test-application: [ 'react-send-to-sentry', @@ -1159,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 }}) @@ -1169,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: @@ -1203,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 diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index f861f4b4ae3b..65e691e2ff0b 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: diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 77d327ba1015..c82b0765e68a 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -8,7 +8,8 @@ current state. Prerequisites: Docker - Copy `.env.example` to `.env` -- Fill in auth information in `.env` for an example Sentry project +- 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 To finally run all of the tests: 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/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-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/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/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/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index e4eb48f03076..a6f0822da0fa 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -19,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; @@ -172,7 +172,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P 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 @@ -195,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); From 4018f80851c4c9b7cf822279e1b054bc667bf3f5 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:16:43 +0200 Subject: [PATCH 07/17] fix(hapi): Specify error channel to filter boom errors (#12725) If errors are handled with Boom inside `onPreResponse`, the error should not be reported to Sentry. fixes https://github.com/getsentry/sentry-javascript/issues/12702 --- .../test-applications/node-hapi/package.json | 3 +- .../test-applications/node-hapi/src/app.js | 50 +++ .../node-hapi/tests/errors.test.ts | 94 ++++ .../node-hapi/tests/transactions.test.ts | 19 + .../node-integration-tests/package.json | 2 +- .../src/integrations/tracing/hapi/index.ts | 2 +- yarn.lock | 420 +++++++++--------- 7 files changed, 371 insertions(+), 219 deletions(-) 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/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/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/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== From e0adb41a2f44540b30f2c5e58f4be98a3bab2ba0 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:36:00 +0200 Subject: [PATCH 08/17] ref(scope): Delete unused public `getStack()` (#12737) `getStack()` is a leftover from the Hub API and was removed with v8. The method does not need to be public anymore and therefore deleted. --- packages/core/src/asyncContext/stackStrategy.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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(); } } From f8f3c981fd28db7b2da66d8c6d8d519d92091d31 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 4 Jul 2024 15:49:43 +0200 Subject: [PATCH 09/17] docs(e2e): Be more explicit about when to run yarn build:tarball in e2e tests (#12762) --- dev-packages/e2e-tests/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index c82b0765e68a..5463bb3eb7a9 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -10,7 +10,8 @@ Prerequisites: Docker - Copy `.env.example` to `.env` - 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 +- 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: From 41d946e268faf09758ca1a3dd5d910090ae6c0c9 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 4 Jul 2024 15:50:47 +0200 Subject: [PATCH 10/17] feat(nestjs): Add function-level span decorator to nestjs (#12721) --- .../nestjs/src/app.controller.ts | 10 +++ .../nestjs/src/app.service.ts | 20 +++++ .../nestjs/tests/span-decorator.test.ts | 74 +++++++++++++++++++ packages/nestjs/README.md | 18 +++++ packages/nestjs/src/index.ts | 2 + packages/nestjs/src/span-decorator.ts | 25 +++++++ 6 files changed, 149 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts create mode 100644 packages/nestjs/src/span-decorator.ts 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/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; + }; +} From b4d64695816405ad477f7f33f877e2019a99ce61 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 4 Jul 2024 15:07:13 +0100 Subject: [PATCH 11/17] feat(otel): Export & use `spanTimeInputToSeconds` for otel span exporter (#12699) Adds support for cases where the time from OTEL is just a number in milliseconds instead of tuples. --- packages/core/src/index.ts | 1 + packages/core/src/utils/spanUtils.ts | 2 +- packages/opentelemetry/src/spanExporter.ts | 12 ++++++------ .../src/utils/convertOtelTimeToSeconds.ts | 4 ---- .../test/utils/convertOtelTimeToSeconds.test.ts | 9 --------- 5 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts delete mode 100644 packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts 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/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); - }); -}); From eedd7c0670d358daf37a6056ef539935527fee8d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 4 Jul 2024 16:20:23 +0200 Subject: [PATCH 12/17] ci: Fix external contributor workflow (#12755) This failed (again) here: https://github.com/getsentry/sentry-javascript/actions/runs/9791997167/job/27036907594 Hopefully this fixes it... --- .github/workflows/external-contributors.yml | 2 -- 1 file changed, 2 deletions(-) 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: From 0bad43bc2c0a12a168112d58460840dc62c9f437 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 5 Jul 2024 02:58:51 -0400 Subject: [PATCH 13/17] fix(deno): Publish from build directory (#12773) In https://github.com/getsentry/sentry-javascript/pull/12700 I adjusted the prepack script to bring back the build directory, but I forgot to adjust `npm pack X` to make sure we pack the tarball from the `build` directory. This patch fixes that. --- packages/deno/package.json | 4 ++-- packages/deno/scripts/prepack.js | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) 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 { From 0951036d8f574d7e8dd55e665a90657725d1fb59 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 5 Jul 2024 09:12:08 +0200 Subject: [PATCH 14/17] build: Ensure we use pnpm 9.4.0 everywhere on CI (#12772) Just to align stuff properly everywhere! --- .github/workflows/build.yml | 2 +- .github/workflows/canary.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 779e221b093c..2ef8d14b6d3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1252,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 65e691e2ff0b..9dcbd43e5547 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -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 From 1845bf44fc2ad63df0115cd832c74b0fcae083b8 Mon Sep 17 00:00:00 2001 From: Julian Hundeloh <5358638+jaulz@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:37:30 +0200 Subject: [PATCH 15/17] fix(core): Pass origin as referrer for `lazyLoadIntegration` (#12766) In our case the referrer is not passed to the feedback script and thus results in a 403 (because we set allowed domains in our project settings). Consequently also the modal doesn't open and it's a bit disappointing for us since we would like to use the feature. --- packages/browser/src/utils/lazyLoadIntegration.ts | 1 + 1 file changed, 1 insertion(+) 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()); From 69b65312689acab9667f8a570cf253f2733ae8e0 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 5 Jul 2024 09:42:35 +0200 Subject: [PATCH 16/17] fix(react): Revert back to `jsxRuntime: 'classic'` to prevent breaking react 17 (#12775) Undoes some of the changes in https://github.com/getsentry/sentry-javascript/pull/12204 and https://github.com/getsentry/sentry-javascript/pull/12740 to fix https://github.com/getsentry/sentry-javascript/issues/12608. --- dev-packages/rollup-utils/npmHelpers.mjs | 4 ++-- .../rollup-utils/plugins/make-esm-plugin.mjs | 15 --------------- packages/react/rollup.npm.config.mjs | 4 +++- packages/remix/rollup.npm.config.mjs | 4 +++- 4 files changed, 8 insertions(+), 19 deletions(-) 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/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'`) }, }), From 1af7d0aa9f6ee67021137319550f503d3fb5b490 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 5 Jul 2024 09:56:42 +0200 Subject: [PATCH 17/17] meta(changelog): Update changelog for 8.15.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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