diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fd537898b28..73e5a1c6ee3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -904,6 +904,7 @@ jobs: 'nextjs-13', 'nextjs-14', 'nextjs-15', + 'nextjs-turbo', 'nextjs-t3', 'react-17', 'react-19', @@ -1126,6 +1127,12 @@ jobs: - test-application: 'nextjs-15' build-command: 'test:build-latest' label: 'nextjs-15 (latest)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-canary' + label: 'nextjs-turbo (canary)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-latest' + label: 'nextjs-turbo (latest)' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 4b8fff855049..b964e6b3d1b0 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -96,6 +96,12 @@ jobs: - test-application: 'nextjs-15' build-command: 'test:build-latest' label: 'nextjs-15 (latest)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-canary' + label: 'nextjs-turbo (canary)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-latest' + label: 'nextjs-turbo (latest)' - test-application: 'react-create-hash-router' build-command: 'test:build-canary' label: 'react-create-hash-router (canary)' diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore new file mode 100644 index 000000000000..ebdbfc025b6a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore @@ -0,0 +1,46 @@ +# 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 + +# Sentry +.sentryclirc + +.vscode + +test-results +event-dumps diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.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-turbo/app/[param]/rsc-page-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx new file mode 100644 index 000000000000..a6ae11918445 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx @@ -0,0 +1,9 @@ +export const dynamic = 'force-dynamic'; + +export default function Page() { + if (Math.random() > -1) { + throw new Error('page rsc render error'); + } + + return null; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx new file mode 100644 index 000000000000..912ad3606a61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx @@ -0,0 +1,27 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { useEffect } from 'react'; + +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string }; +}) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/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-turbo/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts new file mode 100644 index 000000000000..964f937c439a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts new file mode 100644 index 000000000000..40c3d68096c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js new file mode 100644 index 000000000000..e09e64bac6a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js @@ -0,0 +1,12 @@ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + turbo: {}, // Enables Turbopack for builds + }, +}; + +module.exports = withSentryConfig(nextConfig, { + silent: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json new file mode 100644 index 000000000000..900e0b5b2efc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -0,0 +1,49 @@ +{ + "name": "create-next-app", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@canary && pnpm add react-dom@canary && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@rc && pnpm add react-dom@rc && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@types/node": "18.11.17", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "15.0.0", + "react": "rc", + "react-dom": "rc", + "typescript": "4.9.5" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry-internal/feedback": "latest || *", + "@sentry-internal/replay-canvas": "latest || *", + "@sentry-internal/browser-utils": "latest || *", + "@sentry/browser": "latest || *", + "@sentry/core": "latest || *", + "@sentry/nextjs": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@sentry/react": "latest || *", + "@sentry-internal/replay": "latest || *", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", + "@sentry/vercel-edge": "latest || *", + "import-in-the-middle": "1.11.2" + }, + "overrides": { + "import-in-the-middle": "1.11.2" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs new file mode 100644 index 000000000000..a62bec62a5c8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs @@ -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 --turbo' : '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-turbo/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/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-turbo/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts new file mode 100644 index 000000000000..067d2ead0b8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts @@ -0,0 +1,13 @@ +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, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts new file mode 100644 index 000000000000..067d2ead0b8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts @@ -0,0 +1,13 @@ +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, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs new file mode 100644 index 000000000000..2773cf8fa977 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs @@ -0,0 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-turbo', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/nextjs-turbo-${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts new file mode 100644 index 000000000000..604faae7ea59 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Should capture errors from server components', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-turbo', errorEvent => { + return !!errorEvent?.exception?.values?.some(value => value.value === 'page rsc render error'); + }); + + await page.goto(`/123/rsc-page-error`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json new file mode 100644 index 000000000000..ef9e351d7a7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/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"] +}