From d2984588f47d629a364821545fada2f1cbdfbcc1 Mon Sep 17 00:00:00 2001 From: Ziyad Date: Thu, 11 Jul 2024 01:07:59 +0300 Subject: [PATCH] e2e(nextjs): add nextjs app dir project with page extensions --- .../.gitignore | 45 +++++++ .../.npmrc | 2 + .../app/(nested-layout)/layout.custom.tsx | 12 ++ .../nested-layout/layout.custom.tsx | 12 ++ .../nested-layout/page.custom.tsx | 11 ++ .../app/client-component/error.custom.tsx | 11 ++ .../app/client-component/layout.custom.tsx | 10 ++ .../app/client-component/loading.custom.tsx | 7 + .../app/client-component/not-found.custom.tsx | 7 + .../app/client-component/page.custom.tsx | 12 ++ .../[...parameters]/error.custom.tsx | 11 ++ .../[...parameters]/layout.custom.tsx | 10 ++ .../[...parameters]/loading.custom.tsx | 7 + .../[...parameters]/not-found.custom.tsx | 7 + .../parameter/[...parameters]/page.custom.tsx | 15 +++ .../parameter/[parameter]/error.custom.tsx | 11 ++ .../parameter/[parameter]/layout.custom.tsx | 10 ++ .../parameter/[parameter]/loading.custom.tsx | 7 + .../[parameter]/not-found.custom.tsx | 7 + .../parameter/[parameter]/page.custom.tsx | 15 +++ .../error/page.custom.tsx | 12 ++ .../edge-server-components/page.custom.tsx | 13 ++ .../app/error.custom.tsx | 11 ++ .../app/layout.custom.tsx | 44 +++++++ .../app/loading.custom.tsx | 7 + .../app/not-found.custom.tsx | 10 ++ .../app/page.custom.tsx | 10 ++ .../[param]/edge/route.custom.ts | 16 +++ .../[param]/error/route.custom.ts | 9 ++ .../route-handlers/[param]/route.custom.ts | 9 ++ .../app/route-handlers/static/route.custom.ts | 11 ++ .../app/server-action/page.custom.tsx | 43 ++++++ .../app/server-component/error.custom.tsx | 11 ++ .../server-component/faulty/page.custom.tsx | 15 +++ .../app/server-component/layout.custom.tsx | 8 ++ .../app/server-component/loading.custom.tsx | 7 + .../app/server-component/not-found.custom.tsx | 7 + .../not-found/page.custom.tsx | 7 + .../app/server-component/page.custom.tsx | 12 ++ .../[...parameters]/error.custom.tsx | 11 ++ .../[...parameters]/layout.custom.tsx | 8 ++ .../[...parameters]/loading.custom.tsx | 7 + .../[...parameters]/not-found.custom.tsx | 7 + .../parameter/[...parameters]/page.custom.tsx | 13 ++ .../parameter/[parameter]/error.custom.tsx | 11 ++ .../parameter/[parameter]/layout.custom.tsx | 8 ++ .../parameter/[parameter]/loading.custom.tsx | 7 + .../[parameter]/not-found.custom.tsx | 7 + .../parameter/[parameter]/page.custom.tsx | 13 ++ .../server-component/redirect/page.custom.tsx | 7 + .../app/very-slow-component/page.custom.tsx | 6 + .../assert-build.ts | 31 +++++ .../components/client-error-debug-tools.tsx | 124 ++++++++++++++++++ .../components/span-context.tsx | 40 ++++++ .../globals.d.ts | 4 + .../instrumentation.custom.ts | 17 +++ .../middleware.custom.ts | 24 ++++ .../next-env.d.ts | 6 + .../next.config.js | 17 +++ .../package.json | 48 +++++++ .../api/async-context-edge-endpoint.custom.ts | 27 ++++ .../pages/api/edge-endpoint.custom.ts | 23 ++++ .../api/endpoint-behind-middleware.custom.ts | 9 ++ .../pages/api/endpoint.custom.ts | 9 ++ .../pages/api/error-edge-endpoint.custom.ts | 10 ++ .../api/request-instrumentation.custom.ts | 17 +++ .../pages-router/ssr-error-class.custom.tsx | 16 +++ .../pages-router/ssr-error-fc.custom.tsx | 18 +++ .../playwright.config.mjs | 19 +++ .../sentry.client.config.ts | 9 ++ .../start-event-proxy.mjs | 6 + .../tests/async-context-edge.test.ts | 20 +++ ...client-app-routing-instrumentation.test.ts | 52 ++++++++ .../tests/client-errors.test.ts | 38 ++++++ .../connected-servercomponent-trace.test.ts | 21 +++ .../tests/devErrorSymbolification.test.ts | 35 +++++ .../tests/edge-route.test.ts | 65 +++++++++ .../tests/edge.test.ts | 37 ++++++ .../tests/middleware.test.ts | 107 +++++++++++++++ .../tests/pages-ssr-errors.test.ts | 46 +++++++ .../tests/request-instrumentation.test.ts | 24 ++++ .../tests/route-handlers.test.ts | 119 +++++++++++++++++ .../tests/server-components.test.ts | 119 +++++++++++++++++ .../tests/transactions.test.ts | 112 ++++++++++++++++ .../tsconfig.json | 30 +++++ 85 files changed, 1895 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/error/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/edge/route.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/error/route.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/route.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/static/route.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-action/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/faulty/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/error.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/layout.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/loading.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/not-found.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/redirect/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/very-slow-component/page.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/assert-build.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/components/client-error-debug-tools.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/components/span-context.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/instrumentation.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/middleware.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next.config.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/async-context-edge-endpoint.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/edge-endpoint.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint-behind-middleware.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/error-edge-endpoint.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/request-instrumentation.custom.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-class.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-fc.custom.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/sentry.client.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/async-context-edge.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-app-routing-instrumentation.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/connected-servercomponent-trace.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/devErrorSymbolification.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge-route.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/middleware.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/pages-ssr-errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/request-instrumentation.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/route-handlers.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/server-components.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.gitignore new file mode 100644 index 000000000000..e799cc33c4e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/.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-app-dir-with-page-extensions/app/(nested-layout)/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/layout.custom.tsx new file mode 100644 index 000000000000..ace0c2f086b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/layout.custom.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +export const dynamic = 'force-dynamic'; + +export default function Layout({ children }: PropsWithChildren<{}>) { + return ( +
+

Layout

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/layout.custom.tsx new file mode 100644 index 000000000000..ace0c2f086b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/layout.custom.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +export const dynamic = 'force-dynamic'; + +export default function Layout({ children }: PropsWithChildren<{}>) { + return ( +
+

Layout

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/page.custom.tsx new file mode 100644 index 000000000000..8077c14d23ca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/(nested-layout)/nested-layout/page.custom.tsx @@ -0,0 +1,11 @@ +export const dynamic = 'force-dynamic'; + +export default function Page() { + return

Hello World!

; +} + +export async function generateMetadata() { + return { + title: 'I am generated metadata', + }; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/error.custom.tsx new file mode 100644 index 000000000000..c1e3874f7ea8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/client-component)

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/layout.custom.tsx new file mode 100644 index 000000000000..7b447d23cbf8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/layout.custom.tsx @@ -0,0 +1,10 @@ +'use client'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+

Layout (/client-component)

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/loading.custom.tsx new file mode 100644 index 000000000000..9b6cf994d322 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/loading.custom.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+

Loading (/client-component)

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/not-found.custom.tsx new file mode 100644 index 000000000000..deef8f3078b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/not-found.custom.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not found (/client-component)

; +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/page.custom.tsx new file mode 100644 index 000000000000..64012f948278 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/page.custom.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { ClientErrorDebugTools } from '../../components/client-error-debug-tools'; + +export default function Page() { + return ( +
+

Page (/client-component)

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/error.custom.tsx new file mode 100644 index 000000000000..8c52619b80b1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/client-component/[...parameters])

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/layout.custom.tsx new file mode 100644 index 000000000000..a387722a8fcf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/layout.custom.tsx @@ -0,0 +1,10 @@ +'use client'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+

Layout (/client-component/[...parameters])

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/loading.custom.tsx new file mode 100644 index 000000000000..27a8d577240e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/loading.custom.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+

Loading (/client-component/parameter/[...parameters])

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/not-found.custom.tsx new file mode 100644 index 000000000000..c56f5aa409f5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/not-found.custom.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not found (/client-component/[...parameters])

; +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/page.custom.tsx new file mode 100644 index 000000000000..31fa4ee21be5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[...parameters]/page.custom.tsx @@ -0,0 +1,15 @@ +import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; + +export default function Page({ params }: { params: Record }) { + return ( +
+

Page (/client-component/[...parameters])

+

Params: {JSON.stringify(params['parameters'])}

+ +
+ ); +} + +export async function generateStaticParams() { + return [{ parameters: ['foo', 'bar', 'baz'] }]; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/error.custom.tsx new file mode 100644 index 000000000000..92948207a2fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/client-component/[parameter])

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/layout.custom.tsx new file mode 100644 index 000000000000..0d13dbc6bcac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/layout.custom.tsx @@ -0,0 +1,10 @@ +'use client'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+

Layout (/client-component/[parameter])

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/loading.custom.tsx new file mode 100644 index 000000000000..94ad013d3986 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/loading.custom.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+

Loading (/client-component/[parameter])

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/not-found.custom.tsx new file mode 100644 index 000000000000..48c215c930e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/not-found.custom.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not found (/client-component/[parameter])

; +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/page.custom.tsx new file mode 100644 index 000000000000..2b9c28b922ac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/client-component/parameter/[parameter]/page.custom.tsx @@ -0,0 +1,15 @@ +import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; + +export default function Page({ params }: { params: Record }) { + return ( +
+

Page (/client-component/[parameter])

+

Parameter: {JSON.stringify(params['parameter'])}

+ +
+ ); +} + +export async function generateStaticParams() { + return [{ parameter: '42' }]; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/error/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/error/page.custom.tsx new file mode 100644 index 000000000000..1a86e2ac59cf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/error/page.custom.tsx @@ -0,0 +1,12 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export const dynamic = 'force-dynamic'; + +export const runtime = 'edge'; + +export default async function Page() { + 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 + throw new Error('Edge Server Component Error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/page.custom.tsx new file mode 100644 index 000000000000..9d6ec241fca6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/edge-server-components/page.custom.tsx @@ -0,0 +1,13 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export const dynamic = 'force-dynamic'; + +export const runtime = 'edge'; + +export default async function Page() { + 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 + + return

Hello world!

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/error.custom.tsx new file mode 100644 index 000000000000..02a192259aec --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/)

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/layout.custom.tsx new file mode 100644 index 000000000000..d2aae8c9cd8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/layout.custom.tsx @@ -0,0 +1,44 @@ +import Link from 'next/link'; +import { SpanContextProvider } from '../components/span-context'; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + +
+

Layout (/)

+
    +
  • + / +
  • +
  • + /client-component +
  • +
  • + /client-component/parameter/42 +
  • +
  • + /client-component/parameter/foo/bar/baz +
  • +
  • + /server-component +
  • +
  • + /server-component/parameter/42 +
  • +
  • + /server-component/parameter/foo/bar/baz +
  • +
  • + /not-found +
  • +
  • + /redirect +
  • +
+ {children} +
+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/loading.custom.tsx new file mode 100644 index 000000000000..1c89093040e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/loading.custom.tsx @@ -0,0 +1,7 @@ +export default function Loading() { + return ( +
+

Loading (/)

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/not-found.custom.tsx new file mode 100644 index 000000000000..87ce52a19e73 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/not-found.custom.tsx @@ -0,0 +1,10 @@ +import { ClientErrorDebugTools } from '../components/client-error-debug-tools'; + +export default function NotFound() { + return ( +
+

Not found (/)

; + +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/page.custom.tsx new file mode 100644 index 000000000000..edaffa368ace --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/page.custom.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-with-page-extensions/app/route-handlers/[param]/edge/route.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/edge/route.custom.ts new file mode 100644 index 000000000000..8879a85c488a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/edge/route.custom.ts @@ -0,0 +1,16 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; +import { NextResponse } from 'next/server'; + +export const runtime = 'edge'; + +export async function PATCH() { + return NextResponse.json({ name: 'John Doe' }, { status: 401 }); +} + +export async function DELETE(): Promise { + 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 + + throw new Error('route-handler-edge-error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/error/route.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/error/route.custom.ts new file mode 100644 index 000000000000..e873849d22df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/error/route.custom.ts @@ -0,0 +1,9 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export async function PUT(): Promise { + 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 + + throw new Error('route-handler-error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/route.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/route.custom.ts new file mode 100644 index 000000000000..386b8c6e117f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/[param]/route.custom.ts @@ -0,0 +1,9 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ name: 'John Doe' }); +} + +export async function POST() { + return NextResponse.json({ name: 'John Doe' }, { status: 404 }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/static/route.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/static/route.custom.ts new file mode 100644 index 000000000000..174fb171780e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/route-handlers/static/route.custom.ts @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ result: 'static response' }); +} + +// This export makes it so that this route is always dynamically rendered (i.e Sentry will trace) +export const revalidate = 0; + +// This export makes it so that this route will throw an error if the Request object is accessed in some way. +export const dynamic = 'error'; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-action/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-action/page.custom.tsx new file mode 100644 index 000000000000..6784970d2aae --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-action/page.custom.tsx @@ -0,0 +1,43 @@ +import * as Sentry from '@sentry/nextjs'; +import { headers } from 'next/headers'; +import { notFound } from 'next/navigation'; + +export default function ServerComponent() { + async function myServerAction(formData: FormData) { + 'use server'; + return await Sentry.withServerActionInstrumentation( + 'myServerAction', + { formData, headers: headers(), recordResponse: true }, + async () => { + await fetch('http://example.com/'); + return { city: 'Vienna' }; + }, + ); + } + + async function notFoundServerAction(formData: FormData) { + 'use server'; + return await Sentry.withServerActionInstrumentation( + 'notFoundServerAction', + { formData, headers: headers(), recordResponse: true }, + () => { + notFound(); + }, + ); + } + + return ( + <> + {/* @ts-ignore */} +
+ + +
+ {/* @ts-ignore */} +
+ + +
+ + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/error.custom.tsx new file mode 100644 index 000000000000..8c728017a4c9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/server-component)

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/faulty/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/faulty/page.custom.tsx new file mode 100644 index 000000000000..f31b3f1899da --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/faulty/page.custom.tsx @@ -0,0 +1,15 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export const dynamic = 'force-dynamic'; + +export default async function FaultyServerComponent() { + 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 (Math.random() + 1 > 0) { + throw new Error('I am a faulty server component'); + } + + return null; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/layout.custom.tsx new file mode 100644 index 000000000000..3e6a95d2bc49 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/layout.custom.tsx @@ -0,0 +1,8 @@ +export default async function Layout({ children }: { children: React.ReactNode }) { + return ( +
+

Layout (/server-component)

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/loading.custom.tsx new file mode 100644 index 000000000000..70deffd9dea6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/loading.custom.tsx @@ -0,0 +1,7 @@ +export default async function Loading() { + return ( +
+

Loading (/server-component)

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found.custom.tsx new file mode 100644 index 000000000000..57b040b4210d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found.custom.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not found (/server-component)

; +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found/page.custom.tsx new file mode 100644 index 000000000000..c88c2d097d4f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/not-found/page.custom.tsx @@ -0,0 +1,7 @@ +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function Page() { + notFound(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/page.custom.tsx new file mode 100644 index 000000000000..d318ee23968d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/page.custom.tsx @@ -0,0 +1,12 @@ +import { ClientErrorDebugTools } from '../../components/client-error-debug-tools'; + +export const dynamic = 'force-dynamic'; + +export default function Page() { + return ( +
+

Page (/server-component)

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/error.custom.tsx new file mode 100644 index 000000000000..44c78430b2aa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/server-component/[...parameters])

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/layout.custom.tsx new file mode 100644 index 000000000000..b34d5a11488f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/layout.custom.tsx @@ -0,0 +1,8 @@ +export default async function Layout({ children }: { children: React.ReactNode }) { + return ( +
+

Layout (/server-component/[...parameters])

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/loading.custom.tsx new file mode 100644 index 000000000000..f0fa262fa780 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/loading.custom.tsx @@ -0,0 +1,7 @@ +export default async function Loading() { + return ( +
+

Loading (/server-component/[...parameters])

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/not-found.custom.tsx new file mode 100644 index 000000000000..30da42c88a17 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/not-found.custom.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not found (/server-component/[...parameters])

; +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/page.custom.tsx new file mode 100644 index 000000000000..5d9d6c8262c5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[...parameters]/page.custom.tsx @@ -0,0 +1,13 @@ +import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; + +export const dynamic = 'force-dynamic'; + +export default async function Page({ params }: { params: Record }) { + return ( +
+

Page (/server-component/[...parameters])

+

Params: {JSON.stringify(params['parameters'])}

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/error.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/error.custom.tsx new file mode 100644 index 000000000000..37ba7515505f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/error.custom.tsx @@ -0,0 +1,11 @@ +'use client'; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Error (/server-component/[parameter])

+ + Error: {error.toString()} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/layout.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/layout.custom.tsx new file mode 100644 index 000000000000..013b62f15ff5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/layout.custom.tsx @@ -0,0 +1,8 @@ +export default async function Layout({ children }: { children: React.ReactNode }) { + return ( +
+

Layout (/server-component/[parameter])

+ {children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/loading.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/loading.custom.tsx new file mode 100644 index 000000000000..6fb1e6e9d479 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/loading.custom.tsx @@ -0,0 +1,7 @@ +export default async function Loading() { + return ( +
+

Loading (/server-component/[parameter])

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/not-found.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/not-found.custom.tsx new file mode 100644 index 000000000000..9150cdeca2ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/not-found.custom.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not found (/server-component/[parameter])

; +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/page.custom.tsx new file mode 100644 index 000000000000..f88fe1cd4a06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/parameter/[parameter]/page.custom.tsx @@ -0,0 +1,13 @@ +import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; + +export const dynamic = 'force-dynamic'; + +export default async function Page({ params }: { params: Record }) { + return ( +
+

Page (/server-component/[parameter])

+

Parameter: {JSON.stringify(params['parameter'])}

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/redirect/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/redirect/page.custom.tsx new file mode 100644 index 000000000000..3df1746a97ff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/server-component/redirect/page.custom.tsx @@ -0,0 +1,7 @@ +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function Page() { + redirect('/'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/very-slow-component/page.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/very-slow-component/page.custom.tsx new file mode 100644 index 000000000000..ab40d1e62d5f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/app/very-slow-component/page.custom.tsx @@ -0,0 +1,6 @@ +export const dynamic = 'force-dynamic'; + +export default async function SuperSlowPage() { + await new Promise(resolve => setTimeout(resolve, 10000)); + return null; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/assert-build.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/assert-build.ts new file mode 100644 index 000000000000..58453223a4cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/assert-build.ts @@ -0,0 +1,31 @@ +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'); + +// Assert that there was no funky build time warning when we are on a stable (pinned) version +if (nextjsVersion !== 'latest' && !nextjsVersion.includes('-canary') && !nextjsVersion.includes('-rc')) { + assert.doesNotMatch(buildStderr, /Import trace for requested module/); // This is Next.js/Webpack speech for "something is off" +} + +// Assert that all static components stay static and all dynamic components stay dynamic +assert.match(buildStdout, /○ \/client-component/); +assert.match(buildStdout, /● \/client-component\/parameter\/\[\.\.\.parameters\]/); +assert.match(buildStdout, /● \/client-component\/parameter\/\[parameter\]/); +assert.match(buildStdout, /(λ|ƒ) \/server-component/); +assert.match(buildStdout, /(λ|ƒ) \/server-component\/parameter\/\[\.\.\.parameters\]/); +assert.match(buildStdout, /(λ|ƒ) \/server-component\/parameter\/\[parameter\]/); + +// Read the contents of the directory +const files = fs.readdirSync(path.join(process.cwd(), '.next', 'server')); +const mapFiles = files.filter(file => path.extname(file) === '.map'); +if (mapFiles.length > 0) { + throw new Error('.map files found even though `sourcemaps.deleteSourcemapsAfterUpload` option is set!'); +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/components/client-error-debug-tools.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/components/client-error-debug-tools.tsx new file mode 100644 index 000000000000..278da75e850c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/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-app-dir-with-page-extensions/components/span-context.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/components/span-context.tsx new file mode 100644 index 000000000000..81a6f404d0f4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/components/span-context.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { startInactiveSpan } from '@sentry/nextjs'; +import { Span } from '@sentry/types'; +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-app-dir-with-page-extensions/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/instrumentation.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/instrumentation.custom.ts new file mode 100644 index 000000000000..cd269ab160e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/instrumentation.custom.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.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-app-dir-with-page-extensions/middleware.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/middleware.custom.ts new file mode 100644 index 000000000000..6096fcfb1493 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/middleware.custom.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'], +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next-env.d.ts new file mode 100644 index 000000000000..fd36f9494e2c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next.config.js new file mode 100644 index 000000000000..358104c58808 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/next.config.js @@ -0,0 +1,17 @@ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + pageExtensions: ['custom.tsx', 'custom.ts', 'custom.jsx', 'custom.js'], + experimental: { + appDir: true, + serverActions: true, + }, +}; + +module.exports = withSentryConfig(nextConfig, { + debug: true, + sourcemaps: { + deleteSourcemapsAfterUpload: true, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/package.json new file mode 100644 index 000000000000..8ccad25e6ab4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/package.json @@ -0,0 +1,48 @@ +{ + "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: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 && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build", + "test:build-13": "pnpm install && pnpm add next@13.4.19 && npx playwright install && pnpm build", + "test:assert": "pnpm test:test-build && 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": "14.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "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 || *", + "ts-node": "10.9.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/async-context-edge-endpoint.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/async-context-edge-endpoint.custom.ts new file mode 100644 index 000000000000..6dc023fdf1ed --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/async-context-edge-endpoint.custom.ts @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/nextjs'; + +export const config = { + runtime: 'edge', +}; + +export default async function handler() { + // Without a working async context strategy the two spans created by `Sentry.startSpan()` would be nested. + + const outerSpanPromise = Sentry.withIsolationScope(() => { + return Sentry.startSpan({ name: 'outer-span' }, () => { + return new Promise(resolve => setTimeout(resolve, 300)); + }); + }); + + setTimeout(() => { + Sentry.withIsolationScope(() => { + return Sentry.startSpan({ name: 'inner-span' }, () => { + return new Promise(resolve => setTimeout(resolve, 100)); + }); + }); + }, 100); + + await outerSpanPromise; + + return new Response('ok', { status: 200 }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/edge-endpoint.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/edge-endpoint.custom.ts new file mode 100644 index 000000000000..6236aa63d936 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/edge-endpoint.custom.ts @@ -0,0 +1,23 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export const config = { + runtime: 'edge', +}; + +export default async function handler() { + 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 + + return new Response( + JSON.stringify({ + name: 'Jim Halpert', + }), + { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }, + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint-behind-middleware.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint-behind-middleware.custom.ts new file mode 100644 index 000000000000..2ca75a33ba7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint-behind-middleware.custom.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +type Data = { + name: string; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint.custom.ts new file mode 100644 index 000000000000..2ca75a33ba7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/endpoint.custom.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +type Data = { + name: string; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/error-edge-endpoint.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/error-edge-endpoint.custom.ts new file mode 100644 index 000000000000..ed1a0acdf412 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/error-edge-endpoint.custom.ts @@ -0,0 +1,10 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export const config = { runtime: 'edge' }; + +export default () => { + 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 + throw new Error('Edge Route Error'); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/request-instrumentation.custom.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/request-instrumentation.custom.ts new file mode 100644 index 000000000000..044731530152 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/api/request-instrumentation.custom.ts @@ -0,0 +1,17 @@ +import { get } from 'http'; +import { NextApiRequest, NextApiResponse } from 'next'; + +export default (_req: NextApiRequest, res: NextApiResponse) => { + // make an outgoing request in order to test that the `Http` integration creates a span + get('http://example.com/', message => { + message.on('data', () => { + // Noop consuming some data so that request can close :) + }); + + message.on('end', () => { + setTimeout(() => { + res.status(200).json({ message: 'Hello from Next.js!' }); + }, 500); + }); + }); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-class.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-class.custom.tsx new file mode 100644 index 000000000000..86ce68c1c034 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-class.custom.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export default class Page extends React.Component { + render() { + throw new Error('Pages SSR Error Class'); + return
Hello world!
; + } +} + +export function getServerSideProps() { + return { + props: { + foo: 'bar', + }, + }; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-fc.custom.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-fc.custom.tsx new file mode 100644 index 000000000000..552aeae3b331 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/pages/pages-router/ssr-error-fc.custom.tsx @@ -0,0 +1,18 @@ +import { getDefaultIsolationScope } from '@sentry/core'; +import * as Sentry from '@sentry/nextjs'; + +export default function Page() { + 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 + + throw new Error('Pages SSR Error FC'); + return
Hello world!
; +} + +export function getServerSideProps() { + return { + props: { + foo: 'bar', + }, + }; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/playwright.config.mjs new file mode 100644 index 000000000000..0a8d75c8d846 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/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' : '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: '50%', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/sentry.client.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/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-app-dir-with-page-extensions/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/start-event-proxy.mjs new file mode 100644 index 000000000000..7e8016bf98a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-app-dir', +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/async-context-edge.test.ts new file mode 100644 index 000000000000..ecce719f0656 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/async-context-edge.test.ts @@ -0,0 +1,20 @@ +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 => { + return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint'; + }); + + await request.get('/api/async-context-edge-endpoint'); + + const asyncContextEdgerouteTransaction = await edgerouteTransactionPromise; + + const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span'); + const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span'); + + // @ts-expect-error parent_span_id exists + expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); + // @ts-expect-error parent_span_id exists + expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-app-routing-instrumentation.test.ts new file mode 100644 index 000000000000..8645d36c4c8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-app-routing-instrumentation.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Creates a pageload transaction for app router routes', async ({ page }) => { + const randomRoute = String(Math.random()); + + const clientPageloadTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { + return ( + transactionEvent?.transaction === `/server-component/parameter/${randomRoute}` && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/server-component/parameter/${randomRoute}`); + + expect(await clientPageloadTransactionPromise).toBeDefined(); +}); + +test('Creates a navigation transaction for app router routes', async ({ page }) => { + const randomRoute = String(Math.random()); + + const clientPageloadTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { + return ( + transactionEvent?.transaction === `/server-component/parameter/${randomRoute}` && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/server-component/parameter/${randomRoute}`); + await clientPageloadTransactionPromise; + await page.getByText('Page (/server-component/[parameter])').isVisible(); + + const clientNavigationTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { + return ( + transactionEvent?.transaction === '/server-component/parameter/foo/bar/baz' && + transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /server-component/parameter/foo/bar/baz' && + (await clientNavigationTransactionPromise).contexts?.trace?.trace_id === + transactionEvent.contexts?.trace?.trace_id + ); + }); + + await page.getByText('/server-component/parameter/foo/bar/baz').click(); + + expect(await clientNavigationTransactionPromise).toBeDefined(); + expect(await serverComponentTransactionPromise).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-errors.test.ts new file mode 100644 index 000000000000..d1ea09ac2c76 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/client-errors.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +const packageJson = require('../package.json'); + +test('Sends a client-side exception to Sentry', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + const isDevMode = process.env.TEST_ENV === 'development'; + + await page.goto('/'); + + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; + }); + + await page.getByText('Throw error').click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Click Error'); + + expect(errorEvent.request).toEqual({ + headers: expect.any(Object), + url: 'http://localhost:3030/', + }); + + expect(errorEvent.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + // 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, + trace_id: expect.any(String), + span_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/connected-servercomponent-trace.test.ts new file mode 100644 index 000000000000..3d2f29358d54 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/connected-servercomponent-trace.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Will create a transaction with spans for every server component and metadata generation functions when visiting a page', async ({ + page, +}) => { + const serverTransactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /nested-layout'; + }); + + await page.goto('/nested-layout'); + + const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => { + return span.description; + }); + + expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout)/nested-layout)'); + expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout))'); + expect(spanDescriptions).toContainEqual('Page Server Component (/(nested-layout)/nested-layout)'); + expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/devErrorSymbolification.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/devErrorSymbolification.test.ts new file mode 100644 index 000000000000..d1ca11ad9a9e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/devErrorSymbolification.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('dev mode error symbolification', () => { + if (process.env.TEST_ENV !== 'development') { + test.skip('should be skipped for non-dev mode', () => {}); + return; + } + + test('should have symbolicated dev errors', async ({ page }) => { + await page.goto('/'); + + const errorEventPromise = waitForError('nextjs-app-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-with-page-extensions/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge-route.test.ts new file mode 100644 index 000000000000..df7ce7afd19a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge-route.test.ts @@ -0,0 +1,65 @@ +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 => { + return ( + transactionEvent?.transaction === 'GET /api/edge-endpoint' && transactionEvent?.contexts?.trace?.status === 'ok' + ); + }); + + const response = await request.get('/api/edge-endpoint', { + headers: { + 'x-yeet': 'test-value', + }, + }); + expect(await response.json()).toStrictEqual({ name: 'Jim Halpert' }); + + const edgerouteTransaction = await edgerouteTransactionPromise; + + expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge'); + expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value'); +}); + +test('Should create a transaction with error status for faulty edge routes', async ({ request }) => { + const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/error-edge-endpoint' && + transactionEvent?.contexts?.trace?.status === 'internal_error' + ); + }); + + request.get('/api/error-edge-endpoint').catch(() => { + // Noop + }); + + const edgerouteTransaction = await edgerouteTransactionPromise; + + expect(edgerouteTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge'); + + // Assert that isolation scope works properly + expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); +}); + +test('Should record exceptions for faulty edge routes', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error'; + }); + + request.get('/api/error-edge-endpoint').catch(() => { + // Noop + }); + + const errorEvent = await errorEventPromise; + + // Assert that isolation scope works properly + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + + expect(errorEvent.transaction).toBe('GET /api/error-edge-endpoint'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge.test.ts new file mode 100644 index 000000000000..f5277dee6f66 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/edge.test.ts @@ -0,0 +1,37 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should record exceptions for faulty edge server components', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Edge Server Component Error'; + }); + + await page.goto('/edge-server-components/error'); + + const errorEvent = await errorEventPromise; + + expect(errorEvent).toBeDefined(); + + // Assert that isolation scope works properly + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + + expect(errorEvent.transaction).toBe(`Page Server Component (/edge-server-components/error)`); +}); + +test('Should record transaction for edge server components', async ({ page }) => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/edge-server-components)'; + }); + + await page.goto('/edge-server-components'); + + const serverComponentTransaction = await serverComponentTransactionPromise; + + expect(serverComponentTransaction).toBeDefined(); + expect(serverComponentTransaction.request?.headers).toBeDefined(); + + // Assert that isolation scope works properly + expect(serverComponentTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(serverComponentTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/middleware.test.ts new file mode 100644 index 000000000000..11a5f48799bd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/middleware.test.ts @@ -0,0 +1,107 @@ +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 => { + return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok'; + }); + + const response = await request.get('/api/endpoint-behind-middleware'); + expect(await response.json()).toStrictEqual({ name: 'John Doe' }); + + const middlewareTransaction = await middlewareTransactionPromise; + + expect(middlewareTransaction.contexts?.trace?.status).toBe('ok'); + expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); + + // Assert that isolation scope works properly + expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); +}); + +test('Should create a transaction with error status for faulty middleware', async ({ request }) => { + const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'internal_error' + ); + }); + + request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { + // Noop + }); + + const middlewareTransaction = await middlewareTransactionPromise; + + expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); +}); + +test('Records exceptions happening in middleware', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error'; + }); + + request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { + // Noop + }); + + const errorEvent = await errorEventPromise; + + // Assert that isolation scope works properly + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(errorEvent.transaction).toBe('middleware'); +}); + +test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => { + const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'middleware' && + !!transactionEvent.spans?.find(span => span.op === 'http.client') + ); + }); + + request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-make-request': '1' } }).catch(() => { + // Noop + }); + + const middlewareTransaction = await middlewareTransactionPromise; + + expect(middlewareTransaction.spans).toEqual( + expect.arrayContaining([ + { + data: { + 'http.method': 'GET', + 'http.response.status_code': 200, + type: 'fetch', + url: 'http://localhost:3030/', + 'http.url': 'http://localhost:3030/', + 'server.address': 'localhost:3030', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.wintercg_fetch', + }, + description: 'GET http://localhost:3030/', + op: 'http.client', + origin: 'auto.http.wintercg_fetch', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + ]), + ); + expect(middlewareTransaction.breadcrumbs).toEqual( + expect.arrayContaining([ + { + category: 'fetch', + data: { __span: expect.any(String), method: 'GET', status_code: 200, url: 'http://localhost:3030/' }, + timestamp: expect.any(Number), + type: 'http', + }, + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/pages-ssr-errors.test.ts new file mode 100644 index 000000000000..a67e4328ba1c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/pages-ssr-errors.test.ts @@ -0,0 +1,46 @@ +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 => { + return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class'; + }); + + const serverComponentTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === '/pages-router/ssr-error-class' && + (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + + await page.goto('/pages-router/ssr-error-class'); + + expect(await errorEventPromise).toBeDefined(); + expect(await serverComponentTransaction).toBeDefined(); +}); + +test('Will capture error for SSR rendering error with a connected trace (Functional Component)', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC'; + }); + + const ssrTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === '/pages-router/ssr-error-fc' && + (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + + await page.goto('/pages-router/ssr-error-fc'); + + const errorEvent = await errorEventPromise; + const ssrTransaction = await ssrTransactionPromise; + + // Assert that isolation scope works properly + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + + // TODO(lforst): Reuse SSR request span isolation scope to fix the following two assertions + // expect(ssrTransaction.tags?.['my-isolated-tag']).toBe(true); + // expect(ssrTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/request-instrumentation.test.ts new file mode 100644 index 000000000000..d032c6985c94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/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-app-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 http://example.com/', + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/route-handlers.test.ts new file mode 100644 index 000000000000..afa02e60884a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/route-handlers.test.ts @@ -0,0 +1,119 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should create a transaction for route handlers', async ({ request }) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /route-handlers/[param]'; + }); + + const response = await request.get('/route-handlers/foo', { headers: { 'x-yeet': 'test-value' } }); + expect(await response.json()).toStrictEqual({ name: 'John Doe' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(routehandlerTransaction.request?.headers?.['x-yeet']).toBe('test-value'); +}); + +test('Should create a transaction for route handlers and correctly set span status depending on http status', async ({ + request, +}) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'POST /route-handlers/[param]'; + }); + + const response = await request.post('/route-handlers/bar'); + expect(await response.json()).toStrictEqual({ name: 'John Doe' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('not_found'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); +}); + +test('Should record exceptions and transactions for faulty route handlers', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'route-handler-error'; + }); + + const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'PUT /route-handlers/[param]/error'; + }); + + await request.put('/route-handlers/baz/error').catch(() => { + // noop + }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + const routehandlerError = await errorEventPromise; + + // Assert that isolation scope works properly + expect(routehandlerTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(routehandlerTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true); + expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(routehandlerTransaction.contexts?.trace?.origin).toBe('auto.function.nextjs'); + + expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error'); + + expect(routehandlerError.transaction).toBe('PUT /route-handlers/[param]/error'); +}); + +test.describe('Edge runtime', () => { + test('should create a transaction for route handlers', async ({ request }) => { + const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge'; + }); + + const response = await request.patch('/route-handlers/bar/edge'); + expect(await response.json()).toStrictEqual({ name: 'John Doe' }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('unauthenticated'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + }); + + test('should record exceptions and transactions for faulty route handlers', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'route-handler-edge-error'; + }); + + const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'DELETE /route-handlers/[param]/edge'; + }); + + await request.delete('/route-handlers/baz/edge').catch(() => { + // noop + }); + + const routehandlerTransaction = await routehandlerTransactionPromise; + const routehandlerError = await errorEventPromise; + + // Assert that isolation scope works properly + expect(routehandlerTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(routehandlerTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true); + expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + + expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); + expect(routehandlerTransaction.contexts?.runtime?.name).toBe('vercel-edge'); + + expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-edge-error'); + expect(routehandlerError.contexts?.runtime?.name).toBe('vercel-edge'); + + expect(routehandlerError.transaction).toBe('DELETE /route-handlers/[param]/edge'); + }); +}); + +test('should not crash route handlers that are configured with `export const dynamic = "error"`', async ({ + request, +}) => { + const response = await request.get('/route-handlers/static'); + expect(await response.json()).toStrictEqual({ result: 'static response' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/server-components.test.ts new file mode 100644 index 000000000000..ba232ad558b0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/server-components.test.ts @@ -0,0 +1,119 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a transaction for a request to app router', async ({ page }) => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { + return transactionEvent?.transaction === 'GET /server-component/parameter/[...parameters]'; + }); + + await page.goto('/server-component/parameter/1337/42'); + + const transactionEvent = await serverComponentTransactionPromise; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'http.route': '/server-component/parameter/[...parameters]', + 'http.status_code': 200, + 'http.target': '/server-component/parameter/1337/42', + 'otel.kind': 'SERVER', + }), + op: 'http.server', + origin: 'auto.http.otel.http', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + request: { + cookies: {}, + headers: expect.any(Object), + url: expect.any(String), + }, + }), + ); + + expect(Object.keys(transactionEvent.request?.headers!).length).toBeGreaterThan(0); + + // The transaction should not contain any spans with the same name as the transaction + // e.g. "GET /server-component/parameter/[...parameters]" + expect( + transactionEvent.spans?.filter(span => { + return span.description === transactionEvent.transaction; + }), + ).toHaveLength(0); +}); + +test('Should not set an error status on an app router transaction when it redirects', async ({ page }) => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /server-component/redirect'; + }); + + await page.goto('/server-component/redirect'); + + const transactionEvent = await serverComponentTransactionPromise; + + expect(transactionEvent.contexts?.trace?.status).not.toBe('internal_error'); +}); + +test('Should set a "not_found" status on a server component span when notFound() is called and the request span should have status ok', async ({ + page, +}) => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /server-component/not-found'; + }); + + await page.goto('/server-component/not-found'); + + const transactionEvent = await serverComponentTransactionPromise; + + // Transaction should have status ok, because the http status is ok, but the server component span should be not_found + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'Page Server Component (/server-component/not-found)', + op: 'function.nextjs', + status: 'not_found', + }), + ); +}); + +test('Should capture an error and transaction for a app router page', async ({ page }) => { + const transactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /server-component/faulty'; + }); + + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'I am a faulty server component'; + }); + + await page.goto('/server-component/faulty'); + + const transactionEvent = await transactionEventPromise; + const errorEvent = await errorEventPromise; + + // Error event should have the right transaction name + expect(errorEvent.transaction).toBe(`Page Server Component (/server-component/faulty)`); + + // Transaction should have status ok, because the http status is ok, but the server component span should be internal_error + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'Page Server Component (/server-component/faulty)', + op: 'function.nextjs', + status: 'internal_error', + }), + ); + + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); + expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/transactions.test.ts new file mode 100644 index 000000000000..42fee84295b8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tests/transactions.test.ts @@ -0,0 +1,112 @@ +import { expect, test } from '@playwright/test'; +import { 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-app-dir', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/', + tags: { runtime: 'browser' }, + transaction_info: { source: 'url' }, + 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.any(String), + trace_id: expect.any(String), + op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', + data: expect.objectContaining({ + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }), + }, + }, + request: { + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://localhost:3030/', + }, + }), + ); +}); + +test('Should send a transaction for instrumented server actions', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14'); + + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'serverAction/myServerAction'; + }); + + await page.goto('/server-action'); + await page.getByText('Run Action').click(); + + const transactionEvent = await serverComponentTransactionPromise; + + expect(transactionEvent).toBeDefined(); + expect(transactionEvent.extra).toMatchObject({ + 'server_action_form_data.some-text-value': 'some-default-value', + server_action_result: { + city: 'Vienna', + }, + }); + + expect(Object.keys(transactionEvent.request?.headers || {}).length).toBeGreaterThan(0); +}); + +test('Should set not_found status for server actions calling notFound()', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14'); + + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'serverAction/notFoundServerAction'; + }); + + await page.goto('/server-action'); + await page.getByText('Run NotFound Action').click(); + + const transactionEvent = await serverComponentTransactionPromise; + + expect(transactionEvent).toBeDefined(); + expect(transactionEvent.contexts?.trace?.status).toBe('not_found'); +}); + +test('Will not include spans in pageload transaction with faulty timestamps for slow loading pages', async ({ + page, +}) => { + const pageloadTransactionEventPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/very-slow-component' + ); + }); + + await page.goto('/very-slow-component'); + + const pageLoadTransaction = await pageloadTransactionEventPromise; + + expect(pageLoadTransaction.spans?.filter(span => span.timestamp! < span.start_timestamp)).toHaveLength(0); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/tsconfig.json new file mode 100644 index 000000000000..bd69196a9ca4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir-with-page-extensions/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" + } + } +}