diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5047e7acd0a..7c05e0644b09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -994,6 +994,7 @@ jobs: 'ember-classic', 'ember-embroider', 'nextjs-app-dir', + 'nextjs-13', 'nextjs-14', 'nextjs-15', 'react-17', diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 42c1594ebc5b..1f584d2a921c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -78,6 +78,12 @@ jobs: - test-application: 'nextjs-app-dir' build-command: 'test:build-latest' label: 'nextjs-app-dir (latest)' + - test-application: 'nextjs-13' + build-command: 'test:build-canary' + label: 'nextjs-13 (canary)' + - test-application: 'nextjs-13' + build-command: 'test:build-latest' + label: 'nextjs-13 (latest)' - test-application: 'nextjs-14' build-command: 'test:build-canary' label: 'nextjs-14 (canary)' diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore new file mode 100644 index 000000000000..b7a8bf3b3701 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +!*.d.ts + +test-results + +.vscode diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/.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/nextjs-13/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + +
{children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx new file mode 100644 index 000000000000..b8109689f986 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx @@ -0,0 +1,3 @@ +export default function PageloadTransactionPage() { + returnPageload Transaction Page
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx new file mode 100644 index 000000000000..9328f85142a8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx @@ -0,0 +1,6 @@ +export const dynamic = 'force-dynamic'; + +export default async function Page() { + throw new Error('RSC error'); + returnHello World
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts new file mode 100644 index 000000000000..1f889238427c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/nextjs'; + +export function register() { + if (process.env.NEXT_RUNTIME === 'nodejs' || process.env.NEXT_RUNTIME === 'edge') { + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test app + bufferSize: 1000, + }, + }); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts new file mode 100644 index 000000000000..fd36f9494e2c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts @@ -0,0 +1,6 @@ +///arrived
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx new file mode 100644 index 000000000000..5b0847bb89fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx @@ -0,0 +1,3 @@ +export default function Page() { + returnpageload test page
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx new file mode 100644 index 000000000000..01b557bdd09f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx @@ -0,0 +1,7 @@ +const WithInitialPropsPage = ({ data }: { data: string }) =>Hello world!
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx new file mode 100644 index 000000000000..6a30e4f8b3a8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + returnhealthy page
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx new file mode 100644 index 000000000000..3627c5088af8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx @@ -0,0 +1,5 @@ +// See _app.tsx for more information why this file exists. + +export default function Page() { + returnfaulty _app getInitialProps
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx new file mode 100644 index 000000000000..a8e097c769a9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx @@ -0,0 +1,15 @@ +import { captureException, showReportDialog } from '@sentry/nextjs'; + +export default function ReportDialogPage() { + return ( + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension new file mode 100644 index 000000000000..e8d58e47f18e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension @@ -0,0 +1,3 @@ +This page simply exists to test the compatibility of Next.js' `pageExtensions` option with our auto wrapping +process. This file should not be turned into a page by Next.js and our webpack loader also shouldn't process it. +This page should not contain valid JavaScript. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts new file mode 100644 index 000000000000..8448829443d6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts @@ -0,0 +1,19 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs new file mode 100644 index 000000000000..9983d484bcbc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-13', +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts new file mode 100644 index 000000000000..d7d08eb22773 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a pageload transaction when the `app` directory is used', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/pageload-transaction' && transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/pageload-transaction`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + breadcrumbs: expect.arrayContaining([ + { + category: 'navigation', + data: { from: '/pageload-transaction', to: '/pageload-transaction' }, + timestamp: expect.any(Number), + }, + ]), + contexts: { + react: { version: expect.any(String) }, + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', + 'sentry.source': 'url', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + environment: 'qa', + request: { + headers: expect.any(Object), + url: expect.stringMatching(/\/pageload-transaction$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/pageload-transaction', + transaction_info: { source: 'url' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts new file mode 100644 index 000000000000..a9fbcdb69a45 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('should send error for faulty click handlers', async ({ page }) => { + const errorPromise = waitForError('nextjs-13', async errorEvent => { + return errorEvent.exception?.values?.[0].value === 'click error'; + }); + + await page.goto('/42/click-error'); + await page.click('#error-button'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); + + const frames = errorEvent?.exception?.values?.[0]?.stacktrace?.frames; + + await test.step('error should have a non-url-encoded top frame in route with parameter', () => { + if (process.env.TEST_ENV === 'development') { + // In dev mode we want to check local source mapping + expect(frames?.[frames.length - 1].filename).toMatch(/\/\[param\]\/click-error.tsx$/); + } else { + expect(frames?.[frames.length - 1].filename).toMatch(/\/\[param\]\/click-error-[a-f0-9]+\.js$/); + } + }); + + await test.step('error should have `in_app`: false for nextjs internal frames', () => { + if (process.env.TEST_ENV !== 'development') { + expect(frames).toContainEqual( + expect.objectContaining({ + filename: expect.stringMatching( + /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ), + in_app: false, + }), + ); + + expect(frames).not.toContainEqual( + expect.objectContaining({ + filename: expect.stringMatching( + /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ), + in_app: true, + }), + ); + } + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts new file mode 100644 index 000000000000..68336c3e5c4e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; + +// This test verifies that a faulty configuration of `getInitialProps` in `_app` will not cause our +// auto - wrapping / instrumentation to throw an error. +// See `_app.tsx` for more information. + +test('should not fail auto-wrapping when `getInitialProps` configuration is faulty.', async ({ page }) => { + await page.goto('/misconfigured-_app-getInitialProps'); + + const serverErrorText = await page.$('//*[contains(text(), "Internal Server Error")]'); + + expect(serverErrorText).toBeFalsy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts new file mode 100644 index 000000000000..5b42e14269bc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should correctly instrument `fetch` for performance tracing', async ({ page }) => { + await page.route('http://example.com/**/*', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + foo: 'bar', + }), + }); + }); + + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return transactionEvent.transaction === '/fetch' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/fetch`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + transaction: '/fetch', + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + }, + }, + }); + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + 'http.method': 'GET', + url: 'http://example.com', + 'http.url': 'http://example.com/', + 'server.address': 'example.com', + type: 'fetch', + 'http.response_content_length': expect.any(Number), + 'http.response.status_code': 200, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + }, + description: 'GET http://example.com', + op: 'http.client', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + status: expect.any(String), + origin: 'auto.http.browser', + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts new file mode 100644 index 000000000000..8bd128896b6c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should report a navigation transaction for pages router navigations', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', 'Test is flakey in dev mode'); + const navigationTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/navigation-target-page' && + transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goto('/foo/navigation-start-page'); + await page.click('#navigation-link'); + + expect(await navigationTransactionPromise).toMatchObject({ + breadcrumbs: expect.arrayContaining([ + { + category: 'navigation', + data: { from: '/foo/navigation-start-page', to: '/foo/navigation-start-page' }, + timestamp: expect.any(Number), + }, + { category: 'ui.click', message: 'body > div#__next > a#navigation-link', timestamp: expect.any(Number) }, + { + category: 'navigation', + data: { from: '/foo/navigation-start-page', to: '/foo/navigation-target-page' }, + timestamp: expect.any(Number), + }, + ]), + contexts: { + trace: { + data: { + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.nextjs.pages_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'navigation', + origin: 'auto.navigation.nextjs.pages_router_instrumentation', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + platform: 'javascript', + request: { + headers: expect.any(Object), + url: expect.stringMatching(/\/foo\/navigation-target-page$/), + }, + spans: expect.arrayContaining([]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/[param]/navigation-target-page', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts new file mode 100644 index 000000000000..8c74b2c99427 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a pageload transaction when the `pages` directory is used', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/pages-pageload' && transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/foo/pages-pageload`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + breadcrumbs: expect.arrayContaining([ + { + category: 'navigation', + data: { from: '/foo/pages-pageload', to: '/foo/pages-pageload' }, + timestamp: expect.any(Number), + }, + ]), + contexts: { + react: { version: expect.any(String) }, + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.pages_router_instrumentation', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + environment: 'qa', + request: { + headers: expect.any(Object), + url: expect.stringMatching(/\/foo\/pages-pageload$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/[param]/pages-pageload', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts new file mode 100644 index 000000000000..386d228ebf0c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; + +test('should show a dialog', async ({ page }) => { + // *= means "containing" + const dialogScriptSelector = 'head > script[src*="/api/embed/error-page"]'; + + await page.goto('/reportDialog'); + + expect(await page.locator(dialogScriptSelector).count()).toEqual(0); + + await page.click('#open-report-dialog'); + + const dialogScript = await page.waitForSelector(dialogScriptSelector, { state: 'attached' }); + const dialogScriptSrc = await (await dialogScript.getProperty('src')).jsonValue(); + + expect(dialogScriptSrc).toMatch(/^http.*\/api\/embed\/error-page\/\?.*/); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts new file mode 100644 index 000000000000..8fbe8ac8b7b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test'; +import { waitForSession } from '@sentry-internal/test-utils'; + +test('should report healthy sessions', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', 'test is flakey in dev mode'); + + const sessionPromise = waitForSession('nextjs-13', session => { + return session.init === true && session.status === 'ok' && session.errors === 0; + }); + + await page.goto('/healthy-session-page'); + + expect(await sessionPromise).toBeDefined(); +}); + +test('should report crashed sessions', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', 'test is flakey in dev mode'); + + const sessionPromise = waitForSession('nextjs-13', session => { + return session.init === false && session.status === 'crashed' && session.errors === 1; + }); + + await page.goto('/crashed-session-page'); + + expect(await sessionPromise).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts new file mode 100644 index 000000000000..22da2071d533 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should propagate serverside `getInitialProps` trace to client', async ({ page }) => { + const pageloadTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withInitialProps' && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withInitialProps' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto(`/42/withInitialProps`); + + const pageloadTransaction = await pageloadTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + + await test.step('should propagate tracing data from server to client', async () => { + const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); + const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); + + const traceId = pageloadTransaction?.contexts?.trace?.trace_id; + + expect(traceId).toBeDefined(); + + expect(nextDataTagValue.props.pageProps.data).toBe('[some getInitialProps data]'); + expect(nextDataTagValue.props.pageProps._sentryTraceData).toBeTruthy(); + expect(nextDataTagValue.props.pageProps._sentryBaggage).toBeTruthy(); + + expect(nextDataTagValue.props.pageProps._sentryTraceData.split('-')[0]).toBe(traceId); + + expect(nextDataTagValue.props.pageProps._sentryBaggage.match(/sentry-trace_id=([a-f0-9]*),/)[1]).toBe(traceId); + }); + + await test.step('should record serverside performance', async () => { + expect(await serverTransactionPromise).toMatchObject({ + contexts: { + trace: { + op: 'http.server', + status: 'ok', + }, + }, + transaction: '/[param]/withInitialProps', + transaction_info: { + source: 'route', + }, + type: 'transaction', + request: { + url: expect.stringMatching(/http.*\/42\/withInitialProps$/), + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts new file mode 100644 index 000000000000..20bbbc9437f6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should record performance for getServerSideProps', async ({ page }) => { + const pageloadTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withServerSideProps' && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withServerSideProps' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto(`/1337/withServerSideProps`); + + const pageloadTransaction = await pageloadTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + + await test.step('should propagate tracing data from server to client', async () => { + const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); + const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); + + const traceId = pageloadTransaction?.contexts?.trace?.trace_id; + + expect(traceId).toBeDefined(); + + expect(nextDataTagValue.props.pageProps.data).toBe('[some getServerSideProps data]'); + expect(nextDataTagValue.props.pageProps._sentryTraceData).toBeTruthy(); + expect(nextDataTagValue.props.pageProps._sentryBaggage).toBeTruthy(); + + expect(nextDataTagValue.props.pageProps._sentryTraceData.split('-')[0]).toBe(traceId); + + expect(nextDataTagValue.props.pageProps._sentryBaggage.match(/sentry-trace_id=([a-f0-9]*),/)[1]).toBe(traceId); + }); + + await test.step('should record serverside performance', async () => { + expect(await serverTransactionPromise).toMatchObject({ + contexts: { + trace: { + op: 'http.server', + status: 'ok', + }, + }, + transaction: '/[param]/withServerSideProps', + transaction_info: { + source: 'route', + }, + type: 'transaction', + request: { + url: expect.stringMatching(/http.*\/1337\/withServerSideProps$/), + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts new file mode 100644 index 000000000000..8a0ed1176142 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts @@ -0,0 +1,122 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a transaction for a CJS pages router API endpoint', async ({ request }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/cjs-api-endpoint' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + const result = (await request.get(`/api/cjs-api-endpoint`)).json(); + + expect(await result).toMatchObject({ success: true }); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'qa', + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint$/), + }, + spans: expect.arrayContaining([]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/cjs-api-endpoint', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('should not mess up require statements in CJS API endpoints', async ({ request }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/cjs-api-endpoint-with-require' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + const result = (await request.get(`/api/cjs-api-endpoint-with-require`)).json(); + + expect(await result).toMatchObject({ success: true }); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'qa', + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint-with-require$/), + }, + spans: expect.arrayContaining([]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/cjs-api-endpoint-with-require', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts new file mode 100644 index 000000000000..b148accf1450 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should not automatically create transactions for routes that were excluded from auto wrapping (string)', async ({ + request, +}) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/endpoint-excluded-with-string' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await (await request.get(`/api/endpoint-excluded-with-string`)).json(); + + let transactionPromiseReceived = false; + transactionPromise.then(() => { + transactionPromiseReceived = true; + }); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(transactionPromiseReceived).toBe(false); +}); + +test('should not automatically create transactions for routes that were excluded from auto wrapping (regex)', async ({ + request, +}) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/endpoint-excluded-with-regex' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await (await request.get(`/api/endpoint-excluded-with-regex`)).json(); + + let transactionPromiseReceived = false; + transactionPromise.then(() => { + transactionPromiseReceived = true; + }); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(transactionPromiseReceived).toBe(false); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts new file mode 100644 index 000000000000..0c99ba302dfa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts @@ -0,0 +1,173 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should report an error event for errors thrown in getServerSideProps', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'getServerSideProps Error'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === '/error-getServerSideProps' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto('/error-getServerSideProps'); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + trace: { span_id: expect.any(String), trace_id: expect.any(String) }, + }, + event_id: expect.any(String), + exception: { + values: [ + { + mechanism: { handled: false, type: 'generic' }, + type: 'Error', + value: 'getServerSideProps Error', + stacktrace: { + frames: expect.arrayContaining([]), + }, + }, + ], + }, + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/error-getServerSideProps/), + }, + timestamp: expect.any(Number), + transaction: 'getServerSideProps (/error-getServerSideProps)', + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 500, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.function.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.function.nextjs', + span_id: expect.any(String), + status: 'internal_error', + trace_id: expect.any(String), + }, + }, + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/error-getServerSideProps/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/error-getServerSideProps', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('Should report an error event for errors thrown in getServerSideProps in pages with custom page extensions', async ({ + page, +}) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'custom page extension error'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === '/customPageExtension' && transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto('/customPageExtension'); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + trace: { span_id: expect.any(String), trace_id: expect.any(String) }, + }, + event_id: expect.any(String), + exception: { + values: [ + { + mechanism: { handled: false, type: 'generic' }, + type: 'Error', + value: 'custom page extension error', + stacktrace: { + frames: expect.arrayContaining([]), + }, + }, + ], + }, + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/customPageExtension/), + }, + timestamp: expect.any(Number), + transaction: 'getServerSideProps (/customPageExtension)', + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 500, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.function.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.function.nextjs', + span_id: expect.any(String), + status: 'internal_error', + trace_id: expect.any(String), + }, + }, + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/customPageExtension/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/customPageExtension', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts new file mode 100644 index 000000000000..12196c08fcc1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should report an error event for errors thrown in pages router api routes', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'api route error'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/[param]/failure-api-route' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + request.get('/api/foo/failure-api-route').catch(e => { + // expected to crash + }); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { span_id: expect.any(String), trace_id: expect.any(String) }, + }, + exception: { + values: [ + { + mechanism: { + data: { + function: 'withSentry', + }, + handled: false, + type: 'instrument', + }, + stacktrace: { frames: expect.arrayContaining([]) }, + type: 'Error', + value: 'api route error', + }, + ], + }, + platform: 'node', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/foo\/failure-api-route$/), + }, + timestamp: expect.any(Number), + transaction: 'GET /api/[param]/failure-api-route', + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 500, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'internal_error', + trace_id: (await errorEventPromise).contexts?.trace?.trace_id, + }, + }, + platform: 'node', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/foo\/failure-api-route$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/[param]/failure-api-route', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('Should report a transaction event for a successful pages router api route', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/[param]/success-api-route' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + request.get('/api/foo/success-api-route').catch(e => { + // we don't care about crashes + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + platform: 'node', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/foo\/success-api-route$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/[param]/success-api-route', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts new file mode 100644 index 000000000000..600e2b6eda53 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Should capture an error thrown in a server component', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'RSC error'; + }); + + await page.goto('/rsc-error'); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { + parent_span_id: expect.any(String), + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + event_id: expect.any(String), + exception: { + values: [ + { + mechanism: { handled: false, type: 'generic' }, + type: 'Error', + value: 'RSC error', + }, + ], + }, + modules: { next: '13.2.0' }, + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + }, + timestamp: expect.any(Number), + transaction: 'Page Server Component (/rsc-error)', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js new file mode 100644 index 000000000000..0e37a4135be4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js @@ -0,0 +1 @@ +throw new Error('I am throwing'); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts new file mode 100644 index 000000000000..27bf728b42a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +const cases = [ + { + name: 'wrappedNoParamURL', + url: `/api/no-params`, + transactionName: 'GET /api/no-params', + }, + { + name: 'wrappedDynamicURL', + url: `/api/dog`, + transactionName: 'GET /api/[param]', + }, + { + name: 'wrappedCatchAllURL', + url: `/api/params/dog/bug`, + transactionName: 'GET /api/params/[...pathParts]', + }, +]; + +cases.forEach(({ name, url, transactionName }) => { + test(`Should capture transactions for routes with various shapes (${name})`, async ({ request }) => { + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + console.log({ t: transactionEvent.transaction }); + return transactionEvent.transaction === transactionName && transactionEvent.contexts?.trace?.op === 'http.server'; + }); + + request.get(url).catch(() => { + // we don't care about crashes + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + platform: 'node', + request: { + url: expect.stringContaining(url), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: transactionName, + transaction_info: { source: 'route' }, + type: 'transaction', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json new file mode 100644 index 000000000000..ef9e351d7a7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ], + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"], + "exclude": ["node_modules", "playwright.config.ts"] +} diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index a6f0822da0fa..01395202d990 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -7,7 +7,7 @@ import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import type { Envelope, EnvelopeItem, Event, SerializedSession } from '@sentry/types'; import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); @@ -368,7 +368,7 @@ export function waitForEnvelopeItem( /** Wait for an error to be sent. */ export function waitForError( proxyServerName: string, - callback: (transactionEvent: Event) => Promise