diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 85bc81d19132..e25c4ec84053 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -1,5 +1,5 @@ { - "name": "create-next-app", + "name": "nextjs-app-dir", "version": "0.1.0", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.gitignore new file mode 100644 index 000000000000..ebdbfc025b6a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.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-pages-dir/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/.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-pages-dir/app/index.tsx b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/app/index.tsx new file mode 100644 index 000000000000..0f86a210c1d2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/app/index.tsx @@ -0,0 +1 @@ +// Without this file (or better said without the app directory), we run into a otel dependency issue on the edge runtime diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/assert-build.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/assert-build.ts new file mode 100644 index 000000000000..b965e5fcd473 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/assert-build.ts @@ -0,0 +1,44 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as assert from 'assert/strict'; + +const packageJson = require('./package.json'); +const nextjsVersion = packageJson.dependencies.next; + +const buildStdout = fs.readFileSync('.tmp_build_stdout', 'utf-8'); +const buildStderr = fs.readFileSync('.tmp_build_stderr', 'utf-8'); + +const getLatestNextVersion = async () => { + try { + const response = await fetch('https://registry.npmjs.org/next/latest'); + const data = await response.json(); + return data.version as string; + } catch { + return '0.0.0'; + } +}; + +(async () => { + // Assert that there was no funky build time warning when we are on a stable (pinned) version + if ( + !nextjsVersion.includes('-canary') && + !nextjsVersion.includes('-rc') && + // If we install latest we cannot assert on "latest" because the package json will contain the actual version number + nextjsVersion !== (await getLatestNextVersion()) + ) { + assert.doesNotMatch( + buildStderr, + /Import trace for requested module/, // This is Next.js/Webpack speech for "something is off" + `The E2E tests detected a build warning in the Next.js build output:\n\n--------------\n\n${buildStderr}\n\n--------------\n\n`, + ); + } + + // Read the contents of the directory + const files = fs.readdirSync(path.join(process.cwd(), '.next', 'static')); + const mapFiles = files.filter(file => path.extname(file) === '.map'); + if (mapFiles.length > 0) { + throw new Error( + 'Client bundle .map files found even though `sourcemaps.deleteSourcemapsAfterUpload` option is set!', + ); + } +})(); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/components/client-error-debug-tools.tsx b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/components/client-error-debug-tools.tsx new file mode 100644 index 000000000000..278da75e850c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/components/client-error-debug-tools.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { captureException } from '@sentry/nextjs'; +import { useContext, useState } from 'react'; +import { SpanContext } from './span-context'; + +export function ClientErrorDebugTools() { + const spanContextValue = useContext(SpanContext); + const [spanName, setSpanName] = useState(''); + + const [isFetchingAPIRoute, setIsFetchingAPIRoute] = useState(); + const [isFetchingEdgeAPIRoute, setIsFetchingEdgeAPIRoute] = useState(); + const [isFetchingExternalAPIRoute, setIsFetchingExternalAPIRoute] = useState(); + const [renderError, setRenderError] = useState(); + + if (renderError) { + throw new Error('Render Error'); + } + + return ( +
+ {spanContextValue.spanActive ? ( + + ) : ( + <> + { + setSpanName(e.target.value); + }} + /> + + + )} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/components/span-context.tsx b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/components/span-context.tsx new file mode 100644 index 000000000000..834ccc3fadf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/components/span-context.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { startInactiveSpan, Span } from '@sentry/nextjs'; +import { PropsWithChildren, createContext, useState } from 'react'; + +export const SpanContext = createContext< + { spanActive: false; start: (spanName: string) => void } | { spanActive: true; stop: () => void } +>({ + spanActive: false, + start: () => undefined, +}); + +export function SpanContextProvider({ children }: PropsWithChildren) { + const [span, setSpan] = useState(undefined); + + return ( + { + span.end(); + setSpan(undefined); + }, + } + : { + spanActive: false, + start: (spanName: string) => { + const span = startInactiveSpan({ name: spanName }); + setSpan(span); + }, + } + } + > + {children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation-client.ts new file mode 100644 index 000000000000..4870c64e7959 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation-client.ts @@ -0,0 +1,11 @@ +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, +}); + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts new file mode 100644 index 000000000000..a95bb9ee95ee --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/instrumentation.ts @@ -0,0 +1,19 @@ +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.0, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, + }); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/middleware.ts new file mode 100644 index 000000000000..abc565f438b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/middleware.ts @@ -0,0 +1,24 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export async function middleware(request: NextRequest) { + Sentry.setTag('my-isolated-tag', true); + Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope + + if (request.headers.has('x-should-throw')) { + throw new Error('Middleware Error'); + } + + if (request.headers.has('x-should-make-request')) { + await fetch('http://localhost:3030/'); + } + + return NextResponse.next(); +} + +// See "Matching Paths" below to learn more +export const config = { + matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'], +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next-env.d.ts new file mode 100644 index 000000000000..725dd6f24515 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// 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-pages-dir/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next.config.js new file mode 100644 index 000000000000..ee7efe23508f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/next.config.js @@ -0,0 +1,15 @@ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + serverActions: true, + }, +}; + +module.exports = withSentryConfig(nextConfig, { + debug: true, + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json new file mode 100644 index 000000000000..03a7efd1d521 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/package.json @@ -0,0 +1,54 @@ +{ + "name": "nextjs-pages-dir", + "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 && pnpm build", + "test:test-build": "pnpm ts-node --script-mode assert-build.ts", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@latest && pnpm add react-dom@latest && pnpm build", + "test:build-13": "pnpm install && pnpm add next@13.5.11 && pnpm build", + "test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@sentry/core": "latest || *", + "@types/node": "^18.19.1", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "14.2.25", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "ts-node": "10.9.1" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "test:build-13", + "label": "nextjs-pages-dir (next@13)" + } + ], + "optionalVariants": [ + { + "build-command": "test:build-canary", + "label": "nextjs-pages-dir (canary)" + }, + { + "build-command": "test:build-latest", + "label": "nextjs-pages-dir (latest)" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/async-context-edge-endpoint.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/async-context-edge-endpoint.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/edge-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/edge-endpoint.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/edge-endpoint.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/edge-endpoint.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/endpoint-behind-faulty-middleware.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/endpoint-behind-faulty-middleware.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/endpoint-behind-middleware.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-middleware.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/endpoint-behind-middleware.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/endpoint.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/endpoint.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/error-edge-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/error-edge-endpoint.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/error-edge-endpoint.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/error-edge-endpoint.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/request-instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/request-instrumentation.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/request-instrumentation.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/api/request-instrumentation.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/index.tsx b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/index.tsx new file mode 100644 index 000000000000..109542e2fba5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/index.tsx @@ -0,0 +1,10 @@ +import { ClientErrorDebugTools } from '../components/client-error-debug-tools'; + +export default function Page() { + return ( +
+

Page (/)

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/pages-router/ssr-error-class.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/pages-router/ssr-error-class.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/pages-router/ssr-error-fc.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/pages/pages-router/ssr-error-fc.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs new file mode 100644 index 000000000000..c675d003853a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/playwright.config.mjs @@ -0,0 +1,13 @@ +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, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/start-event-proxy.mjs new file mode 100644 index 000000000000..00d301804ba1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/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-pages-dir', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/next-pages-dir-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/async-context-edge.test.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/async-context-edge.test.ts index cb92cb2bab49..d823a1cf5605 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/async-context-edge.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Should allow for async context isolation in the edge SDK', async ({ request }) => { - const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const edgerouteTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint' && transactionEvent.contexts?.runtime?.name === 'vercel-edge' diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/devErrorSymbolification.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/devErrorSymbolification.test.ts new file mode 100644 index 000000000000..c846fab3464c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/devErrorSymbolification.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('should have symbolicated dev errors', async ({ page }) => { + test.skip(process.env.TEST_ENV !== 'development', 'should be skipped for non-dev mode'); + + await page.goto('/'); + + const errorEventPromise = waitForError('nextjs-pages-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; + }); + + await page.getByText('Throw error').click(); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'onClick', + filename: 'components/client-error-debug-tools.tsx', + lineno: 54, + colno: expect.any(Number), + in_app: true, + pre_context: [' {'], + context_line: " throw new Error('Click Error');", + post_context: [' }}', ' >', ' Throw error'], + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts similarity index 93% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts index 88460e3ab533..d2ede428b978 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/edge-route.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Should create a transaction for edge routes', async ({ request }) => { - const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const edgerouteTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'GET /api/edge-endpoint' && transactionEvent.contexts?.runtime?.name === 'vercel-edge' @@ -24,14 +24,14 @@ test('Should create a transaction for edge routes', async ({ request }) => { }); test('Faulty edge routes', async ({ request }) => { - const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const edgerouteTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'GET /api/error-edge-endpoint' && transactionEvent.contexts?.runtime?.name === 'vercel-edge' ); }); - const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + const errorEventPromise = waitForError('nextjs-pages-dir', errorEvent => { return ( errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error' && errorEvent.contexts?.runtime?.name === 'vercel-edge' diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts index ebd60b8e3824..b9c0e7b4b602 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Should create a transaction for middleware', async ({ request }) => { - const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const middlewareTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware'; }); @@ -22,11 +22,11 @@ test('Should create a transaction for middleware', async ({ request }) => { }); test('Faulty middlewares', async ({ request }) => { - const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const middlewareTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-faulty-middleware'; }); - const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + const errorEventPromise = waitForError('nextjs-pages-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error'; }); @@ -53,7 +53,7 @@ test('Faulty middlewares', async ({ request }) => { }); test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => { - const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const middlewareTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' && !!transactionEvent.spans?.find(span => span.op === 'http.client') diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/pages-ssr-errors.test.ts similarity index 85% rename from dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts rename to dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/pages-ssr-errors.test.ts index 10a4cd77f111..c3925f52ba48 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/pages-ssr-errors.test.ts @@ -2,11 +2,11 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Will capture error for SSR rendering error with a connected trace (Class Component)', async ({ page }) => { - const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + const errorEventPromise = waitForError('nextjs-pages-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class'; }); - const serverComponentTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const serverComponentTransaction = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'GET /pages-router/ssr-error-class' && (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id @@ -20,11 +20,11 @@ test('Will capture error for SSR rendering error with a connected trace (Class C }); test('Will capture error for SSR rendering error with a connected trace (Functional Component)', async ({ page }) => { - const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + const errorEventPromise = waitForError('nextjs-pages-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC'; }); - const ssrTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + const ssrTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'GET /pages-router/ssr-error-fc' && (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/request-instrumentation.test.ts new file mode 100644 index 000000000000..c65ba88c39c3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/request-instrumentation.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// Note(lforst): I officially declare bancruptcy on this test. I tried a million ways to make it work but it kept flaking. +// Sometimes the request span was included in the handler span, more often it wasn't. I have no idea why. Maybe one day we will +// figure it out. Today is not that day. +test.skip('Should send a transaction with a http span', async ({ request }) => { + const transactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /api/request-instrumentation'; + }); + + await request.get('/api/request-instrumentation'); + + expect((await transactionPromise).spans).toContainEqual( + expect.objectContaining({ + data: expect.objectContaining({ + 'http.method': 'GET', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.http', + }), + description: 'GET https://example.com/', + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts new file mode 100644 index 000000000000..918297898de7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/transactions.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +const packageJson = require('../package.json'); + +test('Sends a pageload transaction', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + const isDevMode = process.env.TEST_ENV === 'development'; + + const pageloadTransactionEventPromise = waitForTransaction('nextjs-pages-dir', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + react: { + version: expect.any(String), + }, + trace: { + // Next.js >= 15 propagates a trace ID to the client via a meta tag. Also, only dev mode emits a meta tag because + // the requested page is static and only in dev mode SSR is kicked off. + parent_span_id: nextjsMajor >= 15 && isDevMode ? expect.any(String) : undefined, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + op: 'pageload', + origin: 'auto.pageload.nextjs.pages_router_instrumentation', + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', + 'sentry.source': 'route', + }), + }, + }, + request: { + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://localhost:3030/', + }, + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json new file mode 100644 index 000000000000..bd69196a9ca4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json @@ -0,0 +1,30 @@ +{ + "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"], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } +}