Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/global-error.tsx
deleted file mode 100644
index 20c175015b03..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/global-error.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client';
-
-import * as Sentry from '@sentry/nextjs';
-import NextError from 'next/error';
-import { useEffect } from 'react';
-
-export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
- useEffect(() => {
- Sentry.captureException(error);
- }, [error]);
-
- return (
-
-
- {/* `NextError` is the default Next.js error page component. Its type
- definition requires a `statusCode` prop. However, since the App Router
- does not expose status codes for errors, we simply pass 0 to render a
- generic error message. */}
-
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/[product]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/[product]/page.tsx
deleted file mode 100644
index cd1e085e2763..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/[product]/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-export const revalidate = 60; // ISR: revalidate every 60 seconds
-export const dynamicParams = true; // Allow dynamic params beyond generateStaticParams
-
-export async function generateStaticParams(): Promise> {
- return [{ product: 'laptop' }, { product: 'phone' }, { product: 'tablet' }];
-}
-
-export default async function ISRProductPage({ params }: { params: Promise<{ product: string }> }) {
- const { product } = await params;
-
- return (
-
- ISR Product: {product}
- {product}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/static/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/static/page.tsx
deleted file mode 100644
index f49605bd9da4..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/static/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-export const revalidate = 60; // ISR: revalidate every 60 seconds
-export const dynamicParams = true;
-
-export async function generateStaticParams(): Promise {
- return [];
-}
-
-export default function ISRStaticPage() {
- return (
-
- ISR Static Page
- static-isr
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/layout.tsx
deleted file mode 100644
index c8f9cee0b787..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/layout.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Layout({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/page.tsx
deleted file mode 100644
index fdb7bc0a40a7..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/page.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-'use client';
-
-import * as Sentry from '@sentry/nextjs';
-
-export default function Page() {
- const handleClick = async () => {
- Sentry.metrics.count('test.page.count', 1, {
- attributes: {
- page: '/metrics',
- 'random.attribute': 'Apples',
- },
- });
- Sentry.metrics.distribution('test.page.distribution', 100, {
- attributes: {
- page: '/metrics',
- 'random.attribute': 'Manzanas',
- },
- });
- Sentry.metrics.gauge('test.page.gauge', 200, {
- attributes: {
- page: '/metrics',
- 'random.attribute': 'Mele',
- },
- });
- await fetch('/metrics/route-handler');
- };
-
- return (
-
- Metrics page
-
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/route-handler/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/route-handler/route.ts
deleted file mode 100644
index 84e81960f9c9..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/route-handler/route.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as Sentry from '@sentry/nextjs';
-
-export const GET = async () => {
- Sentry.metrics.count('test.route.handler.count', 1, {
- attributes: {
- endpoint: '/metrics/route-handler',
- 'random.attribute': 'Potatoes',
- },
- });
- Sentry.metrics.distribution('test.route.handler.distribution', 100, {
- attributes: {
- endpoint: '/metrics/route-handler',
- 'random.attribute': 'Patatas',
- },
- });
- Sentry.metrics.gauge('test.route.handler.gauge', 200, {
- attributes: {
- endpoint: '/metrics/route-handler',
- 'random.attribute': 'Patate',
- },
- });
- return Response.json({ message: 'Bueno' });
-};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/nested-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/nested-rsc-error/[param]/page.tsx
deleted file mode 100644
index 675b248026be..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/nested-rsc-error/[param]/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Suspense } from 'react';
-
-export const dynamic = 'force-dynamic';
-
-export default async function Page() {
- return (
- Loading...}>
- {/* @ts-ignore */}
- ;
-
- );
-}
-
-async function Crash() {
- throw new Error('I am technically uncatchable');
- return unreachable ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/non-isr-test/[item]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/non-isr-test/[item]/page.tsx
deleted file mode 100644
index e0bafdb24181..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/non-isr-test/[item]/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-// No generateStaticParams - this is NOT an ISR page
-export default async function NonISRPage({ params }: { params: Promise<{ item: string }> }) {
- const { item } = await params;
-
- return (
-
- Non-ISR Dynamic Page: {item}
- {item}
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/page.tsx
deleted file mode 100644
index 2bc0a407a355..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Page() {
- return Next 16 test app ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/layout.tsx
deleted file mode 100644
index 1f0cbe478f88..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/layout.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { PropsWithChildren } from 'react';
-
-export const dynamic = 'force-dynamic';
-
-export default async function Layout({ children }: PropsWithChildren) {
- await new Promise(resolve => setTimeout(resolve, 500));
- return <>{children}>;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/page.tsx
deleted file mode 100644
index 689735d61ddf..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-export const dynamic = 'force-dynamic';
-
-export default async function Page() {
- await new Promise(resolve => setTimeout(resolve, 1000));
- return I am page 2 ;
-}
-
-export async function generateMetadata() {
- (await fetch('https://example.com/', { cache: 'no-store' })).text();
-
- return {
- title: 'my title',
- };
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/[two]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/[two]/page.tsx
deleted file mode 100644
index f34461c2bb07..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/[two]/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function ParameterizedPage() {
- return Dynamic page two ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/page.tsx
deleted file mode 100644
index a7d9164c8c03..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function BeepPage() {
- return Beep ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/page.tsx
deleted file mode 100644
index 9fa617a22381..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function ParameterizedPage() {
- return Dynamic page one ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/static/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/static/page.tsx
deleted file mode 100644
index 16ef0482d53b..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/static/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function StaticPage() {
- return Static page ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/page.tsx
deleted file mode 100644
index 4cb811ecf1b4..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import Link from 'next/link';
-
-export default function Page() {
- return (
-
- link
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/to-be-prefetched/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/to-be-prefetched/page.tsx
deleted file mode 100644
index 83aac90d65cf..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/to-be-prefetched/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-export const dynamic = 'force-dynamic';
-
-export default function Page() {
- return Hello ;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/destination/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/destination/page.tsx
deleted file mode 100644
index 5583d36b04b0..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/destination/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function RedirectDestinationPage() {
- return (
-
- Redirect Destination
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/origin/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/origin/page.tsx
deleted file mode 100644
index 52615e0a054b..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/origin/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { redirect } from 'next/navigation';
-
-async function redirectAction() {
- 'use server';
-
- redirect('/redirect/destination');
-}
-
-export default function RedirectOriginPage() {
- return (
- <>
- {/* @ts-ignore */}
-
- >
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/edge/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/edge/route.ts
deleted file mode 100644
index 7cd1fc7e332c..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/edge/route.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { NextResponse } from 'next/server';
-
-export const runtime = 'edge';
-export const dynamic = 'force-dynamic';
-
-export async function GET() {
- return NextResponse.json({ message: 'Hello Edge Route Handler' });
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/node/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/node/route.ts
deleted file mode 100644
index 5bc418f077aa..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/node/route.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { NextResponse } from 'next/server';
-
-export const dynamic = 'force-dynamic';
-
-export async function GET() {
- return NextResponse.json({ message: 'Hello Node Route Handler' });
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/streaming-rsc-error/[param]/client-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/streaming-rsc-error/[param]/client-page.tsx
deleted file mode 100644
index 7b66c3fbdeef..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/streaming-rsc-error/[param]/client-page.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-'use client';
-
-import { use } from 'react';
-
-export function RenderPromise({ stringPromise }: { stringPromise: Promise }) {
- const s = use(stringPromise);
- return <>{s}>;
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/streaming-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/streaming-rsc-error/[param]/page.tsx
deleted file mode 100644
index 9531f9a42139..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/streaming-rsc-error/[param]/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Suspense } from 'react';
-import { RenderPromise } from './client-page';
-
-export const dynamic = 'force-dynamic';
-
-export default async function Page() {
- const crashingPromise = new Promise((_, reject) => {
- setTimeout(() => {
- reject(new Error('I am a data streaming error'));
- }, 100);
- });
-
- return (
- Loading...}>
- ;
-
- );
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/eslint.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/eslint.config.mjs
deleted file mode 100644
index 60f7af38f6c2..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/eslint.config.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-import { dirname } from 'path';
-import { fileURLToPath } from 'url';
-import { FlatCompat } from '@eslint/eslintrc';
-
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-const compat = new FlatCompat({
- baseDirectory: __dirname,
-});
-
-const eslintConfig = [
- ...compat.extends('next/core-web-vitals', 'next/typescript'),
- {
- ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
- },
-];
-
-export default eslintConfig;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation-client.ts
deleted file mode 100644
index a170a388c465..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation-client.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as Sentry from '@sentry/nextjs';
-import type { Log } 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,
- // Verify Log type is available
- beforeSendLog(log: Log) {
- return log;
- },
-});
-
-export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation.ts
deleted file mode 100644
index 964f937c439a..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as Sentry from '@sentry/nextjs';
-
-export async function register() {
- if (process.env.NEXT_RUNTIME === 'nodejs') {
- await import('./sentry.server.config');
- }
-
- if (process.env.NEXT_RUNTIME === 'edge') {
- await import('./sentry.edge.config');
- }
-}
-
-export const onRequestError = Sentry.captureRequestError;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts
deleted file mode 100644
index a6d31cc8c66f..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-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
-
- // Streamed spans carry no scope tags, so the tests read the isolation state from this attribute instead
- const activeSpan = Sentry.getActiveSpan();
- if (activeSpan) {
- Sentry.getRootSpan(activeSpan).setAttribute(
- 'isolation_scope.is_default',
- Sentry.getIsolationScope() === getDefaultIsolationScope(),
- );
- }
-
- if (request.headers.has('x-should-throw')) {
- throw new Error('Middleware Error');
- }
-
- if (request.headers.has('x-should-make-request')) {
- await fetch('http://localhost:3030/');
- }
-
- return NextResponse.next();
-}
-
-// See "Matching Paths" below to learn more
-export const config = {
- matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
-};
-
-export const runtime = 'experimental-edge';
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts
deleted file mode 100644
index 28162396e80e..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { withSentryConfig } from '@sentry/nextjs/config';
-import type { NextConfig } from 'next';
-
-const nextConfig: NextConfig = {};
-
-export default withSentryConfig(nextConfig, {
- silent: true,
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json
deleted file mode 100644
index f0b40e15fb07..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "name": "nextjs-16-cf-workers",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "cf:build": "opennextjs-cloudflare build",
- "cf:preview": "opennextjs-cloudflare preview --var \"E2E_TEST_DSN:$E2E_TEST_DSN\"",
- "build": "next build",
- "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
- "start": "pnpm cf:preview",
- "lint": "eslint",
- "test:prod": "TEST_ENV=production playwright test",
- "test:build": "pnpm install && pnpm cf:build",
- "test:build-canary": "pnpm install && pnpm add next@canary && pnpm cf:build",
- "test:build-latest": "pnpm install && pnpm add next@16.3 && pnpm cf:build",
- "test:assert": "pnpm test:prod"
- },
- "dependencies": {
- "@opentelemetry/api": "^1.9.1",
- "@opennextjs/cloudflare": "^1.14.9",
- "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
- "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
- "next": "16.2.11",
- "react": "19.1.0",
- "react-dom": "19.1.0"
- },
- "devDependencies": {
- "@playwright/test": "~1.63.0",
- "@sentry-internal/test-utils": "link:../../../test-utils",
- "@types/node": "^20",
- "@types/react": "^19",
- "@types/react-dom": "^19",
- "eslint": "^9",
- "eslint-config-next": "^16",
- "typescript": "^5",
- "wrangler": "^4.61.0"
- },
- "volta": {
- "node": "24.15.0",
- "extends": "../../package.json"
- },
- "sentryTest": {
- "variants": [
- {
- "build-command": "pnpm test:build-latest",
- "label": "nextjs-16-cf-workers (latest)"
- }
- ]
- }
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs
deleted file mode 100644
index 0f15639161dd..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-const testEnv = process.env.TEST_ENV;
-
-if (!testEnv) {
- throw new Error('No test env defined');
-}
-
-const getStartCommand = () => {
- if (testEnv === 'production') {
- return 'pnpm cf:preview --port 3030';
- }
-
- throw new Error(`Unknown test env: ${testEnv}`);
-};
-
-const config = getPlaywrightConfig({
- startCommand: getStartCommand(),
- port: 3030,
-});
-
-export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/file.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/file.svg
deleted file mode 100644
index 004145cddf3f..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/file.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/globe.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/globe.svg
deleted file mode 100644
index 567f17b0d7c7..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/next.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/next.svg
deleted file mode 100644
index 5174b28c565c..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/vercel.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/vercel.svg
deleted file mode 100644
index 77053960334e..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/window.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/window.svg
deleted file mode 100644
index b2b2a44f6ebc..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/window.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.edge.config.ts
deleted file mode 100644
index eceebb29d45d..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.edge.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as Sentry from '@sentry/nextjs';
-
-// Only hands over the build release: `withSentry` from `@sentry/nextjs/cloudflare` in worker.ts creates the client and
-// holds the options.
-Sentry.init({
- dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.server.config.ts
deleted file mode 100644
index 1009ee9e1f39..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.server.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as Sentry from '@sentry/nextjs';
-
-// Only hands over the build release and adds the global Next.js event processors and the turbopack tag: `withSentry`
-// from `@sentry/nextjs/cloudflare` in worker.ts creates the client and holds the options.
-Sentry.init({
- dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/start-event-proxy.mjs
deleted file mode 100644
index efb664370443..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/start-event-proxy.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as fs from 'fs';
-import * as path from 'path';
-import { startEventProxyServer } from '@sentry-internal/test-utils';
-
-const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json')));
-
-startEventProxyServer({
- port: 3031,
- proxyServerName: 'nextjs-16-cf-workers',
- envelopeDumpPath: path.join(
- process.cwd(),
- `event-dumps/next-16-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
- ),
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/async-params.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/async-params.test.ts
deleted file mode 100644
index e8160d12aded..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/async-params.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { expect, test } from '@playwright/test';
-import fs from 'fs';
-import { isDevMode } from './isDevMode';
-
-test('should not print warning for async params', async ({ page }) => {
- test.skip(!isDevMode, 'should be skipped for non-dev mode');
- await page.goto('/');
-
- // If the server exits with code 1, the test will fail (see instrumentation.ts)
- const devStdout = fs.readFileSync('.tmp_dev_server_logs', 'utf-8');
- expect(devStdout).not.toContain('`params` should be awaited before using its properties.');
-
- await expect(page.getByText('Next 16 test app')).toBeVisible();
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isDevMode.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isDevMode.ts
deleted file mode 100644
index d2be94232110..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isDevMode.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const isDevMode = !!process.env.TEST_ENV && process.env.TEST_ENV.includes('development');
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
deleted file mode 100644
index fe61a6263913..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => {
- // Navigate to ISR page
- await page.goto('/isr-test/laptop');
-
- // Wait for page to be fully loaded
- await expect(page.locator('#isr-product-id')).toHaveText('laptop');
-
- // Check that sentry-trace and baggage meta tags are removed for ISR pages
- await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
- await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
-});
-
-test('should remove sentry-trace and baggage meta tags on ISR static route', async ({ page }) => {
- // Navigate to ISR static page
- await page.goto('/isr-test/static');
-
- // Wait for page to be fully loaded
- await expect(page.locator('#isr-static-marker')).toHaveText('static-isr');
-
- // Check that sentry-trace and baggage meta tags are removed for ISR pages
- await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
- await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
-});
-
-test('should remove meta tags for different ISR dynamic route values', async ({ page }) => {
- // Test with 'phone' (one of the pre-generated static params)
- await page.goto('/isr-test/phone');
- await expect(page.locator('#isr-product-id')).toHaveText('phone');
-
- await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
- await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
-
- // Test with 'tablet'
- await page.goto('/isr-test/tablet');
- await expect(page.locator('#isr-product-id')).toHaveText('tablet');
-
- await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
- await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
-});
-
-test('should create unique traces for ISR pages on each visit', async ({ page }) => {
- const traceIds: string[] = [];
-
- // Load the same ISR page 5 times to ensure cached HTML meta tags are consistently removed
- for (let i = 0; i < 5; i++) {
- const spanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/isr-test/:product' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- if (i === 0) {
- await page.goto('/isr-test/laptop');
- } else {
- await page.reload();
- }
-
- const span = await spanPromise;
- const traceId = span.trace_id;
-
- expect(traceId).toBeDefined();
- expect(traceId).toMatch(/[a-f0-9]{32}/);
- traceIds.push(traceId!);
- }
-
- // Verify all 5 page loads have unique trace IDs (no reuse of cached/stale meta tags)
- const uniqueTraceIds = new Set(traceIds);
- expect(uniqueTraceIds.size).toBe(5);
-});
-
-test('ISR route should be identified correctly in the route manifest', async ({ page }) => {
- const spanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/isr-test/:product' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- await page.goto('/isr-test/laptop');
- const span = await spanPromise;
-
- // Verify the span is properly parameterized
- expect(span.name).toBe('/isr-test/:product');
- expect(span.attributes['sentry.segment.name.source']?.value).toBe('route');
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/metrics.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/metrics.test.ts
deleted file mode 100644
index 1ccbe00953de..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/metrics.test.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForMetric } from '@sentry-internal/test-utils';
-
-test('Should emit metrics from server and client', async ({ request, page }) => {
- const clientCountPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
- return metric.name === 'test.page.count';
- });
-
- const clientDistributionPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
- return metric.name === 'test.page.distribution';
- });
-
- const clientGaugePromise = waitForMetric('nextjs-16-cf-workers', async metric => {
- return metric.name === 'test.page.gauge';
- });
-
- const serverCountPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
- return metric.name === 'test.route.handler.count';
- });
-
- const serverDistributionPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
- return metric.name === 'test.route.handler.distribution';
- });
-
- const serverGaugePromise = waitForMetric('nextjs-16-cf-workers', async metric => {
- return metric.name === 'test.route.handler.gauge';
- });
-
- await page.goto('/metrics');
- await page.getByText('Emit').click();
- const clientCount = await clientCountPromise;
- const clientDistribution = await clientDistributionPromise;
- const clientGauge = await clientGaugePromise;
- const serverCount = await serverCountPromise;
- const serverDistribution = await serverDistributionPromise;
- const serverGauge = await serverGaugePromise;
-
- expect(clientCount).toMatchObject({
- timestamp: expect.any(Number),
- trace_id: expect.any(String),
- span_id: expect.any(String),
- name: 'test.page.count',
- type: 'counter',
- value: 1,
- attributes: {
- page: { value: '/metrics', type: 'string' },
- 'random.attribute': { value: 'Apples', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
- 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
- },
- });
-
- expect(clientDistribution).toMatchObject({
- timestamp: expect.any(Number),
- trace_id: expect.any(String),
- span_id: expect.any(String),
- name: 'test.page.distribution',
- type: 'distribution',
- value: 100,
- attributes: {
- page: { value: '/metrics', type: 'string' },
- 'random.attribute': { value: 'Manzanas', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
- 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
- },
- });
-
- expect(clientGauge).toMatchObject({
- timestamp: expect.any(Number),
- trace_id: expect.any(String),
- span_id: expect.any(String),
- name: 'test.page.gauge',
- type: 'gauge',
- value: 200,
- attributes: {
- page: { value: '/metrics', type: 'string' },
- 'random.attribute': { value: 'Mele', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
- 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
- },
- });
-
- expect(serverCount).toMatchObject({
- timestamp: expect.any(Number),
- trace_id: expect.any(String),
- name: 'test.route.handler.count',
- type: 'counter',
- value: 1,
- attributes: {
- 'random.attribute': { value: 'Potatoes', type: 'string' },
- endpoint: { value: '/metrics/route-handler', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
- 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
- },
- });
-
- expect(serverDistribution).toMatchObject({
- timestamp: expect.any(Number),
- trace_id: expect.any(String),
- name: 'test.route.handler.distribution',
- type: 'distribution',
- value: 100,
- attributes: {
- 'random.attribute': { value: 'Patatas', type: 'string' },
- endpoint: { value: '/metrics/route-handler', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
- 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
- },
- });
-
- expect(serverGauge).toMatchObject({
- timestamp: expect.any(Number),
- trace_id: expect.any(String),
- name: 'test.route.handler.gauge',
- type: 'gauge',
- value: 200,
- attributes: {
- 'random.attribute': { value: 'Patate', type: 'string' },
- endpoint: { value: '/metrics/route-handler', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
- 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
- },
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts
deleted file mode 100644
index 32d797f6d836..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpans, collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
-import { isDevMode } from './isDevMode';
-
-// `withSentry` from `@sentry/nextjs/cloudflare` wraps the Worker entry, so middleware runs inside the request's
-// `http.server` segment and its span is a child of it rather than a segment of its own.
-test('Should create a span for middleware inside the request segment', async ({ request }) => {
- const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /api/endpoint-behind-middleware');
-
- const response = await request.get('/api/endpoint-behind-middleware');
- expect(await response.json()).toStrictEqual({ name: 'John Doe' });
-
- const spans = await spansPromise;
- const segmentSpan = spans.find(span => span.is_segment)!;
- const middlewareSpan = spans.find(span => span.name === 'middleware GET')!;
-
- expect(spans.filter(span => getSpanOp(span) === 'middleware')).toEqual([middlewareSpan]);
- expect(segmentSpan.attributes['url.path']?.value).toBe('/api/endpoint-behind-middleware');
- expect(segmentSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
- expect(segmentSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' });
- expect(middlewareSpan.is_segment).toBe(false);
- expect(middlewareSpan.parent_span_id).toBe(segmentSpan.span_id);
- expect(middlewareSpan.status).toBe('ok');
- expect(getSpanOp(middlewareSpan)).toBe('middleware');
-});
-
-test('Faulty middlewares', async ({ request }) => {
- test.skip(isDevMode, 'Throwing crashes the dev server atm'); // https://github.com/vercel/next.js/issues/85261
- // The middleware throws, so the request span of `withSentry` keeps the middleware name, like the middleware segment
- // of Node.js.
- const spansPromise = collectStreamedSpans(
- 'nextjs-16-cf-workers',
- spans =>
- spans.some(span => span.is_segment && span.name === 'middleware GET') &&
- spans.some(span => !span.is_segment && span.name === 'middleware GET'),
- );
-
- request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
- // Noop
- });
-
- const spans = await spansPromise;
- const segmentSpan = spans.find(span => span.is_segment)!;
- const middlewareSpan = spans.find(span => !span.is_segment && span.name === 'middleware GET')!;
-
- expect(segmentSpan.status).toBe('error');
- expect(segmentSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
- expect(middlewareSpan.parent_span_id).toBe(segmentSpan.span_id);
- expect(middlewareSpan.status).toBe('error');
- expect(getSpanOp(middlewareSpan)).toBe('middleware');
-});
-
-test('Should trace outgoing fetch requests inside middleware', async ({ request }) => {
- test.skip(isDevMode, 'The fetch requests ends up in a separate tx in dev atm');
-
- // `http.client` span names are low cardinality under span streaming, hence `GET localhost` rather
- // than the full URL.
- const spansPromise = collectStreamedSpans('nextjs-16-cf-workers', spans =>
- spans.some(span => getSpanOp(span) === 'http.client' && span.name === 'GET localhost'),
- );
-
- request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-make-request': '1' } }).catch(() => {
- // Noop
- });
-
- const spans = await spansPromise;
- const fetchSpan = spans.find(span => getSpanOp(span) === 'http.client' && span.name === 'GET localhost')!;
-
- expect(fetchSpan.status).toBe('ok');
- expect(fetchSpan.attributes).toMatchObject({
- 'http.request.method': { value: 'GET', type: 'string' },
- 'http.response.status_code': { value: 200, type: 'integer' },
- 'url.full': { value: 'http://localhost:3030/', type: 'string' },
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/nested-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/nested-rsc-error.test.ts
deleted file mode 100644
index 03f7d224e466..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/nested-rsc-error.test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('Should capture errors from nested server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
- page,
-}) => {
- const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
- return !!errorEvent?.exception?.values?.some(value => value.value === 'I am technically uncatchable');
- });
-
- // Matched on the error's own trace so a span from an earlier spec cannot satisfy the correlation.
- const serverSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', async span => {
- return (
- span.name === 'GET /nested-rsc-error/[param]' &&
- span.is_segment &&
- (await errorEventPromise).contexts?.trace?.trace_id === span.trace_id
- );
- });
-
- await page.goto(`/nested-rsc-error/123`);
- const errorEvent = await errorEventPromise;
- const serverSpan = await serverSpanPromise;
-
- // error event is part of the same trace as the server span
- expect(errorEvent.contexts?.trace?.trace_id).toBe(serverSpan.trace_id);
-
- expect(errorEvent.request).toMatchObject({
- headers: expect.any(Object),
- method: 'GET',
- });
-
- expect(errorEvent.contexts?.nextjs).toEqual({
- route_type: 'render',
- router_kind: 'App Router',
- router_path: '/nested-rsc-error/[param]',
- request_path: '/nested-rsc-error/123',
- });
-
- expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
- handled: false,
- type: 'auto.function.nextjs.on_request_error',
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/pageload-tracing.test.ts
deleted file mode 100644
index d6165244752e..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/pageload-tracing.test.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('App router spans should be attached to the pageload request span', async ({ page }) => {
- const serverSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === 'GET /pageload-tracing' && span.is_segment;
- });
-
- const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/pageload-tracing' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- await page.goto(`/pageload-tracing`);
-
- const [serverSpan, pageloadSpan] = await Promise.all([serverSpanPromise, pageloadSpanPromise]);
-
- expect(pageloadSpan.trace_id).toBeTruthy();
- expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id);
-});
-
-test('extracts HTTP request headers as span attributes', async ({ baseURL }) => {
- const serverSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- const requestId = span.attributes['http.request.header.x-request-id'];
- return (
- span.name === 'GET /pageload-tracing' &&
- span.is_segment &&
- requestId?.type === 'array' &&
- requestId.value[0] === 'nextjs-789'
- );
- });
-
- await fetch(`${baseURL}/pageload-tracing`, {
- headers: {
- 'User-Agent': 'Custom-NextJS-Agent/15.0',
- 'Content-Type': 'text/html',
- 'X-NextJS-Test': 'nextjs-header-value',
- Accept: 'text/html, application/xhtml+xml',
- 'X-Framework': 'Next.js',
- 'X-Request-ID': 'nextjs-789',
- },
- });
-
- const serverSpan = await serverSpanPromise;
-
- expect(serverSpan.attributes).toMatchObject({
- 'http.request.header.user-agent': { value: ['Custom-NextJS-Agent/15.0'], type: 'array' },
- 'http.request.header.content-type': { value: ['text/html'], type: 'array' },
- 'http.request.header.x-nextjs-test': { value: ['nextjs-header-value'], type: 'array' },
- 'http.request.header.accept': { value: ['text/html, application/xhtml+xml'], type: 'array' },
- 'http.request.header.x-framework': { value: ['Next.js'], type: 'array' },
- 'http.request.header.x-request-id': { value: ['nextjs-789'], type: 'array' },
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts
deleted file mode 100644
index 777130edc5b9..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('should create a parameterized pageload span when the `app` directory is used', async ({ page }) => {
- const spanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/parameterized/:one' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- await page.goto(`/parameterized/cappuccino`);
-
- const span = await spanPromise;
-
- expect(span.span_id).toEqual(expect.stringMatching(/[a-f0-9]{16}/));
- expect(span.trace_id).toEqual(expect.stringMatching(/[a-f0-9]{32}/));
- expect(span.attributes).toMatchObject({
- 'sentry.op': { value: 'pageload', type: 'string' },
- 'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
- 'sentry.segment.name.source': { value: 'route', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'react.version': { value: expect.any(String), type: 'string' },
- });
-});
-
-test('should create a span named after the static route when the `app` directory is used', async ({ page }) => {
- const spanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/parameterized/static' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- await page.goto(`/parameterized/static`);
-
- const span = await spanPromise;
-
- expect(span.span_id).toEqual(expect.stringMatching(/[a-f0-9]{16}/));
- expect(span.trace_id).toEqual(expect.stringMatching(/[a-f0-9]{32}/));
- expect(span.attributes).toMatchObject({
- 'sentry.op': { value: 'pageload', type: 'string' },
- 'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
- 'sentry.segment.name.source': { value: 'route', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'react.version': { value: expect.any(String), type: 'string' },
- 'url.template': { value: '/parameterized/static', type: 'string' },
- });
-});
-
-test('should create a partially parameterized pageload span when the `app` directory is used', async ({ page }) => {
- const spanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/parameterized/:one/beep' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- await page.goto(`/parameterized/cappuccino/beep`);
-
- const span = await spanPromise;
-
- expect(span.span_id).toEqual(expect.stringMatching(/[a-f0-9]{16}/));
- expect(span.trace_id).toEqual(expect.stringMatching(/[a-f0-9]{32}/));
- expect(span.attributes).toMatchObject({
- 'sentry.op': { value: 'pageload', type: 'string' },
- 'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
- 'sentry.segment.name.source': { value: 'route', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'react.version': { value: expect.any(String), type: 'string' },
- });
-});
-
-test('should create a nested parameterized pageload span when the `app` directory is used', async ({ page }) => {
- const spanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/parameterized/:one/beep/:two' && getSpanOp(span) === 'pageload' && span.is_segment;
- });
-
- await page.goto(`/parameterized/cappuccino/beep/espresso`);
-
- const span = await spanPromise;
-
- expect(span.span_id).toEqual(expect.stringMatching(/[a-f0-9]{16}/));
- expect(span.trace_id).toEqual(expect.stringMatching(/[a-f0-9]{32}/));
- expect(span.attributes).toMatchObject({
- 'sentry.op': { value: 'pageload', type: 'string' },
- 'sentry.origin': { value: 'auto.pageload.nextjs.app_router_instrumentation', type: 'string' },
- 'sentry.segment.name.source': { value: 'route', type: 'string' },
- 'sentry.environment': { value: 'qa', type: 'string' },
- 'react.version': { value: expect.any(String), type: 'string' },
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
deleted file mode 100644
index 53cebde1ff21..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
-import { isDevMode } from './isDevMode';
-
-test('Prefetch client spans should have a http.request.prefetch attribute', async ({ page }) => {
- test.skip(isDevMode, "Prefetch requests don't have the prefetch header in dev mode");
-
- // The prefetch span is a child of the pageload segment span, which ends last.
- const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', '/prefetching');
-
- await page.goto(`/prefetching`);
-
- // Make it more likely that nextjs prefetches
- await page.hover('#prefetch-link');
-
- const spans = await spansPromise;
-
- expect(spans).toContainEqual(
- expect.objectContaining({
- attributes: expect.objectContaining({
- 'sentry.op': { value: 'http.client', type: 'string' },
- 'http.request.prefetch': { value: true, type: 'boolean' },
- }),
- }),
- );
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/route-handler.test.ts
deleted file mode 100644
index f8a8420f3694..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/route-handler.test.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import test, { expect } from '@playwright/test';
-import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('Should create a span for node route handlers', async ({ request }) => {
- const routehandlerSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === 'GET /route-handler/[xoxo]/node' && span.is_segment;
- });
-
- const response = await request.get('/route-handler/123/node', { headers: { 'x-charly': 'gomez' } });
- expect(await response.json()).toStrictEqual({ message: 'Hello Node Route Handler' });
-
- const routehandlerSpan = await routehandlerSpanPromise;
-
- expect(routehandlerSpan.status).toBe('ok');
- expect(getSpanOp(routehandlerSpan)).toBe('http.server');
- expect(routehandlerSpan.attributes['http.request.header.x-charly']?.value).toEqual(['gomez']);
-});
-
-test('Should create a span for edge route handlers', async ({ request }) => {
- test.skip(true, 'OpenNext does not support the Edge runtime');
- const routehandlerSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === 'GET /route-handler/[xoxo]/edge' && span.is_segment;
- });
-
- const response = await request.get('/route-handler/123/edge', { headers: { 'x-charly': 'gomez' } });
- expect(await response.json()).toStrictEqual({ message: 'Hello Edge Route Handler' });
-
- const routehandlerSpan = await routehandlerSpanPromise;
-
- expect(routehandlerSpan.status).toBe('ok');
- expect(getSpanOp(routehandlerSpan)).toBe('http.server');
- expect(routehandlerSpan.attributes['http.request.header.x-charly']?.value).toEqual(['gomez']);
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-action-redirect.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-action-redirect.test.ts
deleted file mode 100644
index a5961ae41274..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-action-redirect.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('Should handle server action redirect without capturing errors', async ({ page, request }) => {
- // Wait for the initial pageload span
- const pageLoadSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === '/redirect/origin' && span.is_segment;
- });
-
- // Navigate to the origin page
- await page.goto('/redirect/origin');
-
- const pageLoadSpan = await pageLoadSpanPromise;
- expect(pageLoadSpan).toBeDefined();
-
- // Wait for the redirect span
- const redirectSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', span => {
- return span.name === 'GET /redirect/destination' && span.is_segment;
- });
-
- // The error of `/api/test-error`, requested after the redirect, is the sentinel: an error of the redirect would
- // arrive before it.
- const firstErrorPromise = waitForError('nextjs-16-cf-workers', () => true);
-
- // Click the redirect button
- await page.click('button[type="submit"]');
-
- await redirectSpanPromise;
-
- // Verify we got redirected to the destination page
- await expect(page).toHaveURL('/redirect/destination');
-
- await request.get('/api/test-error');
-
- const firstError = await firstErrorPromise;
- expect(firstError.exception?.values?.[0]?.value).toBe('This is a test error from an API route');
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts
deleted file mode 100644
index 344a62127543..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
-
-// Next.js emits these spans itself. The SDK attaches no op, description or function name to
-// them, so asserting `undefined` pins that they stay untouched.
-const nextjsSpan = { op: undefined, description: undefined, codeFunctionName: undefined };
-
-// `withSentry` from `@sentry/nextjs/cloudflare` wraps the Worker entry, so the segment is its `http.server`
-// span, named after the route Next.js resolved.
-test('Sends a span for a request to app router with URL', async ({ page }) => {
- const spansPromise = collectStreamedSpansUntilSegment(
- 'nextjs-16-cf-workers',
- span =>
- span.name === 'GET /parameterized/[one]/beep/[two]' &&
- span.attributes['url.path']?.value === '/parameterized/1337/beep/42',
- );
-
- await page.goto('/parameterized/1337/beep/42');
-
- const spans = await spansPromise;
- const segmentSpan = spans.find(
- span =>
- span.name === 'GET /parameterized/[one]/beep/[two]' &&
- span.is_segment &&
- span.attributes['url.path']?.value === '/parameterized/1337/beep/42',
- )!;
-
- expect(segmentSpan.span_id).toEqual(expect.stringMatching(/[a-f0-9]{16}/));
- expect(segmentSpan.trace_id).toEqual(expect.stringMatching(/[a-f0-9]{32}/));
- expect(segmentSpan.status).toBe('ok');
- expect(segmentSpan.attributes).toMatchObject({
- 'sentry.op': { value: 'http.server', type: 'string' },
- 'sentry.origin': { value: 'auto.http.cloudflare', type: 'string' },
- 'sentry.sample_rate': { value: 1, type: 'integer' },
- 'sentry.segment.name.source': { value: 'route', type: 'string' },
- 'http.request.method': { value: 'GET', type: 'string' },
- 'http.response.status_code': { value: 200, type: 'integer' },
- 'http.route': { value: '/parameterized/[one]/beep/[two]', type: 'string' },
- 'url.path': { value: '/parameterized/1337/beep/42', type: 'string' },
- 'next.route': { value: '/parameterized/[one]/beep/[two]', type: 'string' },
- });
-
- // No child span should share the segment span's name
- expect(spans.filter(span => !span.is_segment && span.name === segmentSpan.name)).toHaveLength(0);
- expect(spans.filter(span => getSpanOp(span) === 'http.server')).toEqual([segmentSpan]);
-});
-
-test('Will create spans for every server component and metadata generation functions when visiting a page', async ({
- page,
-}) => {
- const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /nested-layout');
-
- await page.goto('/nested-layout');
-
- const fullSpans = await spansPromise;
- const spans = fullSpans.map(span => ({
- name: span.name,
- op: getSpanOp(span),
- description: span.attributes['sentry.description']?.value,
- codeFunctionName: span.attributes['code.function.name']?.value,
- }));
-
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'render route (app) /nested-layout' });
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'build component tree' });
- // Server component spans: the name is the low-cardinality `code.function.name`, and the
- // segment each one resolved for is on the description.
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve root layout server component',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve layout server component "(nested-layout)"',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve layout server component "nested-layout"',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Page',
- op: 'function',
- description: 'resolve page server component "/nested-layout"',
- codeFunctionName: 'Page',
- });
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'generateMetadata /(nested-layout)/nested-layout/page' });
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'start response' });
-});
-
-test('Will create spans for every server component and metadata generation functions when visiting a dynamic page', async ({
- page,
-}) => {
- const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /nested-layout/[dynamic]');
-
- await page.goto('/nested-layout/123');
-
- const fullSpans = await spansPromise;
- const spans = fullSpans.map(span => ({
- name: span.name,
- op: getSpanOp(span),
- description: span.attributes['sentry.description']?.value,
- codeFunctionName: span.attributes['code.function.name']?.value,
- }));
-
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'resolve page components' });
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'render route (app) /nested-layout/[dynamic]' });
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'build component tree' });
- // Server component spans: the name is the low-cardinality `code.function.name`, and the
- // segment each one resolved for is on the description.
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve root layout server component',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve layout server component "(nested-layout)"',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve layout server component "nested-layout"',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Layout',
- op: 'function',
- description: 'resolve layout server component "[dynamic]"',
- codeFunctionName: 'Layout',
- });
- expect(spans).toContainEqual({
- name: 'Page',
- op: 'function',
- description: 'resolve page server component "/nested-layout/[dynamic]"',
- codeFunctionName: 'Page',
- });
- expect(spans).toContainEqual({
- ...nextjsSpan,
- name: 'generateMetadata /(nested-layout)/nested-layout/[dynamic]/page',
- });
- expect(spans).toContainEqual({ ...nextjsSpan, name: 'start response' });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts
deleted file mode 100644
index 1b991f1a9fe4..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
-
-test('Should capture errors for crashing streaming promises in server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
- page,
-}) => {
- const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
- return !!errorEvent?.exception?.values?.some(value => value.value === 'I am a data streaming error');
- });
-
- // Matched on the error's own trace so a span from an earlier spec cannot satisfy the correlation.
- const serverSpanPromise = waitForStreamedSpan('nextjs-16-cf-workers', async span => {
- return (
- span.name === 'GET /streaming-rsc-error/[param]' &&
- span.is_segment &&
- (await errorEventPromise).contexts?.trace?.trace_id === span.trace_id
- );
- });
-
- // The streaming RSC error can interrupt the HTTP response, causing the navigation to reject
- // (e.g. net::ERR_ABORTED) even though the error and transaction are still captured.
- await page.goto(`/streaming-rsc-error/123`).catch(() => {});
- const errorEvent = await errorEventPromise;
- const serverSpan = await serverSpanPromise;
-
- // error event is part of the same trace as the server span
- expect(errorEvent.contexts?.trace?.trace_id).toBe(serverSpan.trace_id);
-
- expect(errorEvent.request).toMatchObject({
- headers: expect.any(Object),
- method: 'GET',
- });
-
- expect(errorEvent.contexts?.nextjs).toEqual({
- route_type: 'render',
- router_kind: 'App Router',
- router_path: '/streaming-rsc-error/[param]',
- request_path: '/streaming-rsc-error/123',
- });
-
- expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
- handled: false,
- type: 'auto.function.nextjs.on_request_error',
- });
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/trace-continuation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/trace-continuation.test.ts
deleted file mode 100644
index 447aa7df0cb6..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/trace-continuation.test.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpans } from '@sentry-internal/test-utils';
-
-test('Continues an incoming trace in the segment of the Worker request', async ({ request }) => {
- const traceId = crypto.randomUUID().replace(/-/g, '');
- const parentSpanId = traceId.slice(0, 16);
-
- const spansPromise = collectStreamedSpans(
- 'nextjs-16-cf-workers',
- spans =>
- spans.some(
- span =>
- span.trace_id === traceId &&
- span.is_segment &&
- span.attributes['sentry.origin']?.value === 'auto.http.cloudflare',
- ) &&
- spans.some(
- span => span.trace_id === traceId && span.name === 'render route (app) /parameterized/[one]/beep/[two]',
- ),
- );
-
- await request.get('/parameterized/1337/beep/42', {
- headers: {
- 'sentry-trace': `${traceId}-${parentSpanId}-1`,
- baggage: `sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-sampled=true`,
- },
- });
-
- const spans = await spansPromise;
- const segmentSpan = spans.find(
- span => span.is_segment && span.attributes['sentry.origin']?.value === 'auto.http.cloudflare',
- )!;
- const renderSpan = spans.find(span => span.name === 'render route (app) /parameterized/[one]/beep/[two]')!;
-
- expect(segmentSpan.parent_span_id).toBe(parentSpanId);
- expect(segmentSpan.name).toBe('GET /parameterized/[one]/beep/[two]');
- expect(renderSpan.attributes['sentry.segment.id']?.value).toBe(segmentSpan.span_id);
-});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tsconfig.json
deleted file mode 100644
index cc9ed39b5aa2..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tsconfig.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"],
- "exclude": ["node_modules"]
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc
deleted file mode 100644
index 16b0271383b1..000000000000
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * For more details on how to configure Wrangler, refer to:
- * https://developers.cloudflare.com/workers/wrangler/configuration/
- */
-/**
- * For more details on how to configure Wrangler, refer to:
- * https://developers.cloudflare.com/workers/wrangler/configuration/
- */
-{
- "$schema": "node_modules/wrangler/config-schema.json",
- "name": "next-cf",
- "main": "./worker.ts",
- // From 2026-02-19 on, wrangler uses the native `process` of workerd, whose `cwd()` is `/bundle` during a request.
- // Next.js then misses its router server context and extracts the incoming trace again, see trace-continuation.test.ts.
- "compatibility_date": "2026-02-19",
- "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
- "assets": {
- "binding": "ASSETS",
- "directory": ".open-next/assets",
- },
- "images": {
- // Enable image optimization
- // see https://opennext.js.org/cloudflare/howtos/image
- "binding": "IMAGES",
- },
- "services": [
- {
- // Self-reference service binding, the service name must match the worker name
- // see https://opennext.js.org/cloudflare/caching
- "binding": "WORKER_SELF_REFERENCE",
- "service": "next-cf",
- },
- ],
- "d1_databases": [
- {
- "binding": "DB",
- "database_name": "e2e",
- "database_id": "00000000-0000-0000-0000-000000000000",
- },
- ],
- "observability": {
- "enabled": true,
- },
- /**
- * Smart Placement
- * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
- */
- // "placement": { "mode": "smart" }
- /**
- * Bindings
- * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
- * databases, object storage, AI inference, real-time communication and more.
- * https://developers.cloudflare.com/workers/runtime-apis/bindings/
- */
- /**
- * Environment Variables
- * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
- */
- // "vars": { "MY_VARIABLE": "production_value" }
- /**
- * Note: Use secrets to store sensitive data.
- * https://developers.cloudflare.com/workers/configuration/secrets/
- */
- /**
- * Static Assets
- * https://developers.cloudflare.com/workers/static-assets/binding/
- */
- // "assets": { "directory": "./public/", "binding": "ASSETS" }
- /**
- * Service Bindings (communicate between multiple Workers)
- * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
- */
- // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
-}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/ai-mock-server.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16/ai-mock-server.mjs
index b97e0cb162dc..50060d37c309 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/ai-mock-server.mjs
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/ai-mock-server.mjs
@@ -24,11 +24,12 @@ function sendJson(res, status, obj) {
res.end(JSON.stringify(obj));
}
-let serverPromise;
+/** Port of the mock server. The Playwright global setup starts it, so the app reaches it on every runtime. */
+export const MOCK_AI_PORT = 3032;
-/** Lazily starts the shared mock server and resolves to its port. */
-export function getMockAiPort() {
- serverPromise ??= new Promise(resolve => {
+/** Starts the mock server on `MOCK_AI_PORT` and resolves to a function that stops it. */
+export function startMockAiServer() {
+ return new Promise(resolve => {
const server = createServer(async (req, res) => {
const url = req.url || '';
@@ -84,10 +85,8 @@ export function getMockAiPort() {
res.writeHead(404).end();
});
- server.listen(0, () => {
- resolve(server.address().port);
+ server.listen(MOCK_AI_PORT, () => {
+ resolve(() => new Promise(done => server.close(done)));
});
});
-
- return serverPromise;
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/d1/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/d1/route.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/d1/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/api/d1/route.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/logs/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/logs/route.ts
new file mode 100644
index 000000000000..b289a2365f2a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/logs/route.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+// Without this Next.js prerenders the route at build time, where `init` creates no client and the log is dropped.
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ Sentry.logger.info('e2e server log', { 'e2e.attr': 'value' });
+ return Response.json({ ok: true });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/openai/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/openai/route.ts
index 9315f9d079ce..628b7a239697 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/openai/route.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/openai/route.ts
@@ -1,13 +1,12 @@
import { NextResponse } from 'next/server';
import OpenAI from 'openai';
-import { getMockAiPort } from '../../../ai-mock-server.mjs';
+import { MOCK_AI_PORT } from '../../../ai-mock-server.mjs';
export const dynamic = 'force-dynamic';
export async function GET() {
- const port = await getMockAiPort();
const client = new OpenAI({
- baseURL: `http://localhost:${port}/openai`,
+ baseURL: `http://localhost:${MOCK_AI_PORT}/openai`,
apiKey: 'mock-api-key',
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/otel-context/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/otel-context/route.ts
similarity index 54%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/otel-context/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/api/otel-context/route.ts
index 1ca661b60b63..26199762ea55 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/otel-context/route.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/otel-context/route.ts
@@ -1,4 +1,8 @@
-import { context, createContextKey, trace } from '@opentelemetry/api';
+// The app does not install `@opentelemetry/api`: OpenNext resolves the ESM build of an installed copy when it bundles
+// the Node.js middleware for Workers, but its file tracing only copies the CommonJS build. The copy in Next.js uses the
+// same global OpenTelemetry API, but has no types.
+// @ts-expect-error `next/dist/compiled/@opentelemetry/api` ships no type declarations
+import { context, createContextKey, trace } from 'next/dist/compiled/@opentelemetry/api';
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
@@ -9,7 +13,7 @@ export async function GET() {
const tracer = trace.getTracer('e2e');
context.with(context.active().setValue(E2E_CONTEXT_KEY, 'e2e-value'), () => {
- tracer.startActiveSpan('otel-context-outer', outer => {
+ tracer.startActiveSpan('otel-context-outer', (outer: { end(): void }) => {
// An explicit context, as OpenTelemetry instrumentations pass it.
const inner = tracer.startSpan('otel-context-inner', {}, context.active());
inner.setAttribute('e2e.context.value', String(context.active().getValue(E2E_CONTEXT_KEY)));
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/test-error/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/test-error/route.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/api/test-error/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/api/test-error/route.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch-external-disallowed/route.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch-external-disallowed/route.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/check/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch/check/route.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/check/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch/check/route.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch/route.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/route.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/test-outgoing-fetch/route.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/utils.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/utils.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/utils.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/app/propagation/utils.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/global-setup.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16/global-setup.mjs
index dc7ec8736faf..41ec97defa8f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/global-setup.mjs
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/global-setup.mjs
@@ -1,6 +1,7 @@
import { execSync } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
+import { startMockAiServer } from './ai-mock-server.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -11,4 +12,8 @@ export default async function globalSetup() {
cwd: __dirname,
stdio: 'inherit',
});
+
+ // The mock AI server runs here and not in the app, because a Worker can not start a `node:http` server. Playwright
+ // runs the returned function as teardown.
+ return startMockAiServer();
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts
index 9278da016887..6893bd77fdf7 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts
@@ -5,6 +5,9 @@ import type { NextConfig } from 'next';
process.env.VERCEL = '1';
const nextConfig: NextConfig = {
+ // `pg` requires `pg-cloudflare` on Workers. The file tracing only copies its Node.js build, and OpenNext copies the
+ // whole package (with its `workerd` build) only for packages listed here.
+ serverExternalPackages: ['pg-cloudflare'],
experimental: {
authInterrupts: true,
sri: {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/open-next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/open-next.config.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/open-next.config.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/open-next.config.ts
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
index 408cd3be5a38..80f92437f500 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
@@ -20,10 +20,16 @@
"test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack",
"test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack",
"test:assert": "pnpm test:prod && pnpm test:dev",
- "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
+ "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack",
+ "test:build:cloudflare": "pnpm install && opennextjs-cloudflare build",
+ "test:build-latest:cloudflare": "pnpm install && pnpm add next@16.3 && opennextjs-cloudflare build",
+ "start:cloudflare": "opennextjs-cloudflare preview --var \"E2E_TEST_DSN:$E2E_TEST_DSN\""
},
"//": "Pin `ioredis` to 5.10.1: it's the last version before the driver publishes its own native diagnostics channels; orchestrion's config covers `ioredis <5.11.0`.",
"dependencies": {
+ "@opennextjs/cloudflare": "^1.14.9",
+ "@sentry/bun": "file:../../packed/sentry-bun-packed.tgz",
+ "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@vercel/queue": "^0.1.3",
@@ -47,7 +53,8 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "^16",
- "typescript": "^5"
+ "typescript": "^5",
+ "wrangler": "^4.61.0"
},
"volta": {
"extends": "../../package.json"
@@ -67,6 +74,17 @@
{
"build-command": "pnpm test:build-latest",
"label": "nextjs-16 (latest, turbopack)"
+ },
+ {
+ "runtime": "bun",
+ "assert-command": "pnpm test:prod",
+ "label": "nextjs-16 (bun)"
+ },
+ {
+ "runtime": "cloudflare",
+ "build-command": "pnpm test:build:cloudflare",
+ "assert-command": "pnpm test:prod",
+ "label": "nextjs-16 (cloudflare)"
}
],
"optionalVariants": [
@@ -79,6 +97,18 @@
"build-command": "pnpm test:build-canary-webpack",
"label": "nextjs-16 (canary, webpack)",
"assert-command": "pnpm test:assert-webpack"
+ },
+ {
+ "runtime": "cloudflare",
+ "build-command": "pnpm test:build-latest:cloudflare",
+ "assert-command": "pnpm test:prod",
+ "label": "nextjs-16 (cloudflare, latest)"
+ },
+ {
+ "runtime": "deno",
+ "assert-command": "pnpm test:prod",
+ "deno-version": "v2.9.0",
+ "label": "nextjs-16 (deno)"
}
]
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16/playwright.config.mjs
index cbac80f5b7e7..7e356d81488b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/playwright.config.mjs
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/playwright.config.mjs
@@ -1,10 +1,17 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+import { getPlaywrightConfig, getRuntime } from '@sentry-internal/test-utils';
const testEnv = process.env.TEST_ENV;
if (!testEnv) {
throw new Error('No test env defined');
}
+const productionStartCommands = {
+ node: 'pnpm next start -p 3030',
+ bun: 'bun --bun ./node_modules/next/dist/bin/next start -p 3030',
+ deno: 'deno run -A ./node_modules/next/dist/bin/next start -p 3030',
+ cloudflare: 'pnpm start:cloudflare --port 3030',
+};
+
const getStartCommand = () => {
if (testEnv === 'development-webpack') {
return 'pnpm next dev -p 3030 --webpack 2>&1 | tee .tmp_dev_server_logs';
@@ -15,7 +22,7 @@ const getStartCommand = () => {
}
if (testEnv === 'production') {
- return 'pnpm next start -p 3030';
+ return productionStartCommands[getRuntime()];
}
throw new Error(`Unknown test env: ${testEnv}`);
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts
index 4507c799f9f3..960f49a15533 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts
@@ -1,3 +1,5 @@
+import { bunHttpServerIntegration, fetchIntegration as bunFetchIntegration } from '@sentry/bun';
+import { fetchIntegration as denoFetchIntegration } from '@sentry/deno';
import * as Sentry from '@sentry/nextjs';
import { Log } from '@sentry/nextjs';
@@ -6,8 +8,17 @@ Sentry.init({
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
+ tracePropagationTargets: ['http://localhost:3030/propagation/test-outgoing-fetch/check'],
// debug: true,
- integrations: [Sentry.vercelAIIntegration(), Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })],
+ integrations: [
+ Sentry.vercelAIIntegration(),
+ Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }),
+ // Bun does not emit the `node:http` diagnostics channel the Node SDK uses to isolate incoming requests, so each
+ // request would otherwise share one trace. Next.js emits its own server spans, hence `spans: false`.
+ ...(process.versions.bun ? [bunHttpServerIntegration({ spans: false }), bunFetchIntegration()] : []),
+ // The `fetch` of Deno is not undici, so the Node SDK does not see outgoing requests.
+ ...(process.versions.deno ? [denoFetchIntegration()] : []),
+ ],
// Verify Log type is available
beforeSendLog(log: Log) {
return log;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/cloudflare-runtime.test.ts
similarity index 79%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/tests/cloudflare-runtime.test.ts
index d088edd32dcb..bf1c773190a7 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/cloudflare-runtime.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/cloudflare-runtime.test.ts
@@ -1,9 +1,11 @@
import { expect, test } from '@playwright/test';
-import { waitForError } from '@sentry-internal/test-utils';
+import { getRuntime, waitForError } from '@sentry-internal/test-utils';
+
+test.skip(getRuntime() !== 'cloudflare', 'Checks the runtime context of the Cloudflare Worker');
test.describe('Cloudflare Runtime', () => {
test('Should report cloudflare as the runtime in API route error events', async ({ request }) => {
- const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
+ const errorEventPromise = waitForError('nextjs-16', errorEvent => {
return !!errorEvent?.exception?.values?.some(value =>
value.value?.includes('This is a test error from an API route'),
);
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/d1-binding.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/d1-binding.test.ts
similarity index 82%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/d1-binding.test.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/tests/d1-binding.test.ts
index 43c31b94084d..e86279c27c23 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/d1-binding.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/d1-binding.test.ts
@@ -1,9 +1,11 @@
import { expect, test } from '@playwright/test';
-import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
+import { collectStreamedSpansUntilSegment, getRuntime, getSpanOp } from '@sentry-internal/test-utils';
+
+test.skip(getRuntime() !== 'cloudflare', 'D1 is a binding of Cloudflare Workers');
// `withSentry` passes an instrumented `env` to the OpenNext handler, which `getCloudflareContext()` returns.
test('Should create a span for a D1 query through the `env` of `getCloudflareContext`', async ({ request }) => {
- const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /api/d1');
+ const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'GET /api/d1');
const response = await request.get('/api/d1');
expect(await response.json()).toStrictEqual([{ one: 1 }]);
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts
index 968891a085c4..e155b1bed5a1 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/db-page.test.ts
@@ -1,18 +1,18 @@
import { expect, test } from '@playwright/test';
-import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
+import { collectStreamedSpansUntilSegment, getRuntime } from '@sentry-internal/test-utils';
+
+// pg is externalized, so the runtime module hook instruments it.
+test('Instruments pg calls made during server-side rendering of a page', async ({ page }) => {
+ test.skip(getRuntime() === 'bun' || getRuntime() === 'cloudflare', 'Bun and Workers have no runtime module hook');
-test('Instruments DB calls made during server-side rendering of a page', async ({ page }) => {
// The db spans are children of the segment span, which ends last.
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'GET /db-page');
await page.goto('/db-page');
await expect(page.locator('#answer')).toHaveText('answer: 42');
- await expect(page.locator('#cached')).toHaveText('cached: 42');
const spans = await spansPromise;
- // One page render produces spans from both injection paths: pg (externalized → runtime module
- // hook) and ioredis (bundle-safe allowlisted → build-time loader).
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
@@ -25,6 +25,18 @@ test('Instruments DB calls made during server-side rendering of a page', async (
}),
}),
);
+});
+
+// ioredis is bundle-safe and allowlisted, so the build-time loader instruments it.
+test('Instruments ioredis calls made during server-side rendering of a page', async ({ page }) => {
+ // The db spans are children of the segment span, which ends last.
+ const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'GET /db-page');
+
+ await page.goto('/db-page');
+ await expect(page.locator('#cached')).toHaveText('cached: 42');
+
+ const spans = await spansPromise;
+
expect(spans).toContainEqual(
expect.objectContaining({
name: 'set localhost:6379',
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/logs.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/logs.test.ts
new file mode 100644
index 000000000000..25dbd2a909e6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/logs.test.ts
@@ -0,0 +1,39 @@
+import { expect, test } from '@playwright/test';
+import { getRuntime, waitForEnvelopeItem, waitForStreamedSpan } from '@sentry-internal/test-utils';
+import type { SerializedLogContainer } from '@sentry/core';
+
+test('Sends a log of a route handler in the trace of the request', async ({ request }) => {
+ // Workers have no hostname, so the client of `@sentry/cloudflare` has no `serverName` and its logs no
+ // `server.address`. The other runtimes take `os.hostname()`.
+ const serverAddressType = getRuntime() === 'cloudflare' ? undefined : 'string';
+
+ const segmentSpanPromise = waitForStreamedSpan('nextjs-16', span => {
+ return span.name === 'GET /api/logs' && span.is_segment;
+ });
+
+ const response = await request.get('/api/logs');
+ expect(await response.json()).toStrictEqual({ ok: true });
+
+ const segmentSpan = await segmentSpanPromise;
+ // The log can arrive before the segment, so the search starts at the first envelope of the proxy.
+ const logEnvelopeItem = await waitForEnvelopeItem(
+ 'nextjs-16',
+ envelopeItem =>
+ envelopeItem[0].type === 'log' &&
+ (envelopeItem[1] as SerializedLogContainer).items.some(log => log.trace_id === segmentSpan.trace_id),
+ 0,
+ );
+ const logs = (logEnvelopeItem[1] as SerializedLogContainer).items.filter(
+ log => log.trace_id === segmentSpan.trace_id,
+ );
+
+ expect(logs).toHaveLength(1);
+ expect(logs[0]!.body).toBe('e2e server log');
+ expect(logs[0]!.level).toBe('info');
+ expect(logs[0]!.severity_number).toBe(9);
+ expect(logs[0]!.attributes?.['e2e.attr']).toEqual({ value: 'value', type: 'string' });
+ expect(logs[0]!.attributes?.['sentry.environment']).toEqual({ value: 'qa', type: 'string' });
+ expect(logs[0]!.attributes?.['sentry.sdk.name']).toEqual({ value: 'sentry.javascript.nextjs', type: 'string' });
+ expect(logs[0]!.attributes?.['sentry.trace.parent_span_id']?.value).toMatch(/^[a-f0-9]{16}$/);
+ expect(logs[0]!.attributes?.['server.address']?.type).toBe(serverAddressType);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts
index 43edb917d526..85e7c2635f12 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/metrics.test.ts
@@ -1,7 +1,11 @@
import { expect, test } from '@playwright/test';
-import { waitForMetric } from '@sentry-internal/test-utils';
+import { getRuntime, waitForMetric } from '@sentry-internal/test-utils';
test('Should emit metrics from server and client', async ({ request, page }) => {
+ // Workers have no hostname, so the client of `@sentry/cloudflare` has no `serverName` and its metrics no
+ // `server.address`. The other runtimes take `os.hostname()`.
+ const serverAddressType = getRuntime() === 'cloudflare' ? undefined : 'string';
+
const clientCountPromise = waitForMetric('nextjs-16', async metric => {
return metric.name === 'test.page.count';
});
@@ -90,7 +94,6 @@ test('Should emit metrics from server and client', async ({ request, page }) =>
type: 'counter',
value: 1,
attributes: {
- 'server.address': { value: expect.any(String), type: 'string' },
'random.attribute': { value: 'Potatoes', type: 'string' },
endpoint: { value: '/metrics/route-handler', type: 'string' },
'sentry.environment': { value: 'qa', type: 'string' },
@@ -106,7 +109,6 @@ test('Should emit metrics from server and client', async ({ request, page }) =>
type: 'distribution',
value: 100,
attributes: {
- 'server.address': { value: expect.any(String), type: 'string' },
'random.attribute': { value: 'Patatas', type: 'string' },
endpoint: { value: '/metrics/route-handler', type: 'string' },
'sentry.environment': { value: 'qa', type: 'string' },
@@ -122,7 +124,6 @@ test('Should emit metrics from server and client', async ({ request, page }) =>
type: 'gauge',
value: 200,
attributes: {
- 'server.address': { value: expect.any(String), type: 'string' },
'random.attribute': { value: 'Patate', type: 'string' },
endpoint: { value: '/metrics/route-handler', type: 'string' },
'sentry.environment': { value: 'qa', type: 'string' },
@@ -130,4 +131,7 @@ test('Should emit metrics from server and client', async ({ request, page }) =>
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
},
});
+ expect(serverCount.attributes?.['server.address']?.type).toBe(serverAddressType);
+ expect(serverDistribution.attributes?.['server.address']?.type).toBe(serverAddressType);
+ expect(serverGauge.attributes?.['server.address']?.type).toBe(serverAddressType);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts
index f91168e54038..d3b3b3dcd05c 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts
@@ -2,12 +2,15 @@ import { expect, test } from '@playwright/test';
import {
collectStreamedSpans,
collectStreamedSpansUntilSegment,
+ getRuntime,
getSpanOp,
waitForStreamedSpan,
} from '@sentry-internal/test-utils';
import { isDevMode } from './isDevMode';
test('Should create a span for middleware', async ({ request }) => {
+ test.skip(getRuntime() === 'cloudflare', 'On Workers the middleware span is a child of the request span');
+
const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'middleware GET');
const routeSpanPromise = waitForStreamedSpan('nextjs-16', span => {
@@ -44,8 +47,33 @@ test('Should create a span for middleware', async ({ request }) => {
expect(routeSpan.attributes['isolation_scope.has_proxy_marker']).toEqual({ value: false, type: 'boolean' });
});
+// `withSentry` from `@sentry/nextjs/cloudflare` wraps the Worker entry, so middleware runs inside the request's
+// `http.server` segment and its span is a child of it rather than a segment of its own.
+test('Should create a span for middleware inside the request segment of the Worker', async ({ request }) => {
+ test.skip(getRuntime() !== 'cloudflare', 'Only on Workers the request span of `withSentry` is the segment');
+
+ const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'GET /api/endpoint-behind-middleware');
+
+ const response = await request.get('/api/endpoint-behind-middleware');
+ expect(await response.json()).toStrictEqual({ name: 'John Doe' });
+
+ const spans = await spansPromise;
+ const segmentSpan = spans.find(span => span.is_segment)!;
+ const middlewareSpan = spans.find(span => span.name === 'middleware GET')!;
+
+ expect(spans.filter(span => getSpanOp(span) === 'middleware')).toEqual([middlewareSpan]);
+ expect(segmentSpan.attributes['url.path']?.value).toBe('/api/endpoint-behind-middleware');
+ expect(segmentSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
+ expect(segmentSpan.attributes['isolation_scope.is_default']).toEqual({ value: false, type: 'boolean' });
+ expect(middlewareSpan.is_segment).toBe(false);
+ expect(middlewareSpan.parent_span_id).toBe(segmentSpan.span_id);
+ expect(middlewareSpan.status).toBe('ok');
+ expect(getSpanOp(middlewareSpan)).toBe('middleware');
+});
+
test('Faulty middlewares', async ({ request }) => {
test.skip(isDevMode, 'Throwing crashes the dev server atm'); // https://github.com/vercel/next.js/issues/85261
+ test.skip(getRuntime() === 'cloudflare', 'On Workers the middleware span is a child of the request span');
const middlewareSpanPromise = waitForStreamedSpan('nextjs-16', span => {
return span.name === 'middleware GET' && span.is_segment;
});
@@ -65,9 +93,39 @@ test('Faulty middlewares', async ({ request }) => {
// await test.step('should record exceptions', async () => { ... });
});
+test('Faulty middlewares inside the request segment of the Worker', async ({ request }) => {
+ test.skip(getRuntime() !== 'cloudflare', 'Only on Workers the request span of `withSentry` is the segment');
+ // The middleware throws, so the request span of `withSentry` keeps the middleware name, like the middleware segment
+ // of Node.js.
+ const spansPromise = collectStreamedSpans(
+ 'nextjs-16',
+ spans =>
+ spans.some(span => span.is_segment && span.name === 'middleware GET') &&
+ spans.some(span => !span.is_segment && span.name === 'middleware GET'),
+ );
+
+ request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
+ // Noop
+ });
+
+ const spans = await spansPromise;
+ const segmentSpan = spans.find(span => span.is_segment)!;
+ const middlewareSpan = spans.find(span => !span.is_segment && span.name === 'middleware GET')!;
+
+ expect(segmentSpan.status).toBe('error');
+ expect(segmentSpan.attributes['sentry.segment.name.source']?.value).toBe('route');
+ expect(middlewareSpan.parent_span_id).toBe(segmentSpan.span_id);
+ expect(middlewareSpan.status).toBe('error');
+ expect(getSpanOp(middlewareSpan)).toBe('middleware');
+});
+
test('Should trace outgoing fetch requests inside middleware', async ({ request }) => {
test.skip(isDevMode, 'The fetch requests ends up in a separate tx in dev atm');
+ // Only Node.js has the undici diagnostics channel. On the other runtimes the fetch integration of the runtime
+ // SDK creates this span, which has no `sentry.kind`, `url.path` and `url.scheme`.
+ const isNode = getRuntime() === 'node';
+
// In some builds (especially webpack) the fetch span is not a child of the middleware segment but a
// segment of its own, so this waits for either. `http.client` span names are low cardinality under
// span streaming, hence `GET localhost` rather than the full URL.
@@ -83,17 +141,15 @@ test('Should trace outgoing fetch requests inside middleware', async ({ request
const fetchSpan = spans.find(span => getSpanOp(span) === 'http.client' && span.name === 'GET localhost')!;
expect(fetchSpan.status).toBe('ok');
- expect(fetchSpan.attributes).toMatchObject({
- 'http.request.method': { value: 'GET', type: 'string' },
- 'http.response.status_code': { value: 200, type: 'integer' },
- 'sentry.kind': { value: 'client', type: 'string' },
- 'sentry.op': { value: 'http.client', type: 'string' },
- 'sentry.origin': { value: 'auto.http.node_fetch', type: 'string' },
- 'server.address': { value: 'localhost', type: 'string' },
- 'server.port': { value: 3030, type: 'integer' },
- 'url.domain': { value: 'localhost', type: 'string' },
- 'url.full': { value: 'http://localhost:3030/', type: 'string' },
- 'url.path': { value: '/', type: 'string' },
- 'url.scheme': { value: 'http', type: 'string' },
- });
+ expect(fetchSpan.attributes['http.request.method']?.value).toBe('GET');
+ expect(fetchSpan.attributes['http.response.status_code']?.value).toBe(200);
+ expect(fetchSpan.attributes['sentry.op']?.value).toBe('http.client');
+ expect(fetchSpan.attributes['sentry.origin']?.value).toBe(isNode ? 'auto.http.node_fetch' : 'auto.http.fetch');
+ expect(fetchSpan.attributes['server.address']?.value).toBe('localhost');
+ expect(fetchSpan.attributes['server.port']?.value).toBe(3030);
+ expect(fetchSpan.attributes['url.domain']?.value).toBe('localhost');
+ expect(fetchSpan.attributes['url.full']?.value).toBe('http://localhost:3030/');
+ expect(fetchSpan.attributes['sentry.kind']?.value).toBe(isNode ? 'client' : undefined);
+ expect(fetchSpan.attributes['url.path']?.value).toBe(isNode ? '/' : undefined);
+ expect(fetchSpan.attributes['url.scheme']?.value).toBe(isNode ? 'http' : undefined);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts
index 5295101f1e20..7f611cf3daa9 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/node-runtime-metrics.test.ts
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';
-import { waitForMetric } from '@sentry-internal/test-utils';
+import { getRuntime, waitForMetric } from '@sentry-internal/test-utils';
+
+test.skip(getRuntime() === 'cloudflare', 'Node.js runtime metrics do not exist on Workers');
const EXPECTED_ATTRIBUTES = {
'sentry.environment': { value: 'qa', type: 'string' },
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/otel-context.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/otel-context.test.ts
similarity index 95%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/otel-context.test.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/tests/otel-context.test.ts
index 5303c62a984f..1bc31009a6b1 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/otel-context.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/otel-context.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';
test('Keeps OpenTelemetry context values and parents', async ({ request }) => {
- const spansPromise = collectStreamedSpansUntilSegment('nextjs-16-cf-workers', 'GET /api/otel-context');
+ const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', 'GET /api/otel-context');
const response = await request.get('/api/otel-context');
expect(await response.json()).toStrictEqual({ ok: true });
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/propagation.test.ts
similarity index 73%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/propagation.test.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/tests/propagation.test.ts
index e82fb6453474..e758cd769551 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/propagation.test.ts
@@ -5,12 +5,14 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL, request }
// Inbound span, outbound span and the http.client span in between all share one trace, and
// `collectStreamedSpans` evaluates a single trace at a time, so requiring all three together
// keeps them paired.
- const spansPromise = collectStreamedSpans('nextjs-16-bun', spans => {
+ const spansPromise = collectStreamedSpans('nextjs-16', spans => {
return (
spans.some(span => span.name === 'GET /propagation/test-outgoing-fetch' && span.is_segment) &&
spans.some(span => span.name === 'GET /propagation/test-outgoing-fetch/check' && span.is_segment) &&
spans.some(
- span => getSpanOp(span) === 'http.client' && span.attributes['sentry.origin']?.value === 'auto.http.fetch',
+ span =>
+ getSpanOp(span) === 'http.client' &&
+ String(span.attributes['url.full']?.value).endsWith('/propagation/test-outgoing-fetch/check'),
)
);
});
@@ -23,7 +25,9 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL, request }
span => span.name === 'GET /propagation/test-outgoing-fetch/check' && span.is_segment,
)!;
const httpClientSpan = spans.find(
- span => getSpanOp(span) === 'http.client' && span.attributes['sentry.origin']?.value === 'auto.http.fetch',
+ span =>
+ getSpanOp(span) === 'http.client' &&
+ String(span.attributes['url.full']?.value).endsWith('/propagation/test-outgoing-fetch/check'),
);
expect(inboundSpan.trace_id).toStrictEqual(expect.any(String));
@@ -33,10 +37,10 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL, request }
expect(httpClientSpan?.span_id).toStrictEqual(expect.any(String));
expect(inboundSpan.parent_span_id).toBe(httpClientSpan?.span_id);
- expect(headers).toMatchObject({
- baggage: expect.any(String),
- 'sentry-trace': `${outboundSpan.trace_id}-${httpClientSpan?.span_id}-1`,
- });
+ expect(headers['sentry-trace']).toBe(`${outboundSpan.trace_id}-${httpClientSpan?.span_id}-1`);
+ expect(headers.baggage).toContain(`sentry-trace_id=${outboundSpan.trace_id}`);
+ expect(headers.baggage).toContain('sentry-sampled=true');
+ expect(headers.baggage).toContain('sentry-environment=qa');
});
test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({
@@ -44,11 +48,11 @@ test('Does not propagate outgoing fetch requests not covered by tracePropagation
request,
}) => {
// These two spans are deliberately in different traces, so they are matched by their unique names.
- const inboundSpanPromise = waitForStreamedSpan('nextjs-16-bun', span => {
+ const inboundSpanPromise = waitForStreamedSpan('nextjs-16', span => {
return span.name === 'GET /propagation/test-outgoing-fetch-external-disallowed/check' && span.is_segment;
});
- const outboundSpanPromise = waitForStreamedSpan('nextjs-16-bun', span => {
+ const outboundSpanPromise = waitForStreamedSpan('nextjs-16', span => {
return span.name === 'GET /propagation/test-outgoing-fetch-external-disallowed' && span.is_segment;
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-action-redirect.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-action-redirect.test.ts
index 27b82ae35c61..3ebe985701f4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-action-redirect.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-action-redirect.test.ts
@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
-test('Should handle server action redirect without capturing errors', async ({ page }) => {
+test('Should handle server action redirect without capturing errors', async ({ page, request }) => {
// Wait for the initial pageload span
const pageLoadSpanPromise = waitForStreamedSpan('nextjs-16', span => {
return span.name === '/redirect/origin' && span.is_segment;
@@ -18,10 +18,9 @@ test('Should handle server action redirect without capturing errors', async ({ p
return span.name === 'GET /redirect/destination' && span.is_segment;
});
- // No error should be captured
- const redirectErrorPromise = waitForError('nextjs-16', async errorEvent => {
- return !!errorEvent;
- });
+ // The error of `/api/test-error`, requested after the redirect, is the sentinel: an error of the redirect would
+ // arrive before it.
+ const firstErrorPromise = waitForError('nextjs-16', () => true);
// Click the redirect button
await page.click('button[type="submit"]');
@@ -31,17 +30,8 @@ test('Should handle server action redirect without capturing errors', async ({ p
// Verify we got redirected to the destination page
await expect(page).toHaveURL('/redirect/destination');
- // Wait for potential errors with a 2 second timeout
- const errorTimeout = new Promise((_, reject) =>
- setTimeout(() => reject(new Error('No error captured (timeout)')), 2000),
- );
-
- // We expect this to timeout since no error should be captured during the redirect
- try {
- await Promise.race([redirectErrorPromise, errorTimeout]);
- throw new Error('Expected no error to be captured, but an error was found');
- } catch (e) {
- // If we get a timeout error (as expected), no error was captured
- expect((e as Error).message).toBe('No error captured (timeout)');
- }
+ await request.get('/api/test-error');
+
+ const firstError = await firstErrorPromise;
+ expect(firstError.exception?.values?.[0]?.value).toBe('This is a test error from an API route');
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-components.test.ts
index b271cfd1ac52..e7e0de8b1202 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-components.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/server-components.test.ts
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
-import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils';
+import { collectStreamedSpansUntilSegment, getRuntime, getSpanOp } from '@sentry-internal/test-utils';
import { isTurbopackDevMode } from './isDevMode';
// Next.js emits these spans itself. The SDK attaches no op, description or function name to
@@ -8,6 +8,7 @@ const nextjsSpan = { op: undefined, description: undefined, codeFunctionName: un
test('Sends a span for a request to app router with URL', async ({ page }) => {
test.skip(isTurbopackDevMode, 'Turbopack intermittently returns 404 for nested dynamic routes in dev mode');
+ test.skip(getRuntime() === 'cloudflare', 'On Workers the segment is the request span of `withSentry`');
const spansPromise = collectStreamedSpansUntilSegment(
'nextjs-16',
@@ -45,6 +46,49 @@ test('Sends a span for a request to app router with URL', async ({ page }) => {
// No child span should share the segment span's name
expect(spans.filter(span => !span.is_segment && span.name === segmentSpan.name)).toHaveLength(0);
+ expect(spans.filter(span => getSpanOp(span) === 'http.server')).toEqual([segmentSpan]);
+});
+
+// `withSentry` from `@sentry/nextjs/cloudflare` wraps the Worker entry, so the segment is its `http.server` span with
+// the attributes of `@sentry/cloudflare`, named after the route Next.js resolved.
+test('Sends the request span of the Worker for a request to app router with URL', async ({ page }) => {
+ test.skip(getRuntime() !== 'cloudflare', 'Only on Workers the request span of `withSentry` is the segment');
+
+ const spansPromise = collectStreamedSpansUntilSegment(
+ 'nextjs-16',
+ span =>
+ span.name === 'GET /parameterized/[one]/beep/[two]' &&
+ span.attributes['url.path']?.value === '/parameterized/1337/beep/42',
+ );
+
+ await page.goto('/parameterized/1337/beep/42');
+
+ const spans = await spansPromise;
+ const segmentSpan = spans.find(
+ span =>
+ span.name === 'GET /parameterized/[one]/beep/[two]' &&
+ span.is_segment &&
+ span.attributes['url.path']?.value === '/parameterized/1337/beep/42',
+ )!;
+
+ expect(segmentSpan.span_id).toEqual(expect.stringMatching(/[a-f0-9]{16}/));
+ expect(segmentSpan.trace_id).toEqual(expect.stringMatching(/[a-f0-9]{32}/));
+ expect(segmentSpan.status).toBe('ok');
+ expect(segmentSpan.attributes).toMatchObject({
+ 'sentry.op': { value: 'http.server', type: 'string' },
+ 'sentry.origin': { value: 'auto.http.cloudflare', type: 'string' },
+ 'sentry.sample_rate': { value: 1, type: 'integer' },
+ 'sentry.segment.name.source': { value: 'route', type: 'string' },
+ 'http.request.method': { value: 'GET', type: 'string' },
+ 'http.response.status_code': { value: 200, type: 'integer' },
+ 'http.route': { value: '/parameterized/[one]/beep/[two]', type: 'string' },
+ 'url.path': { value: '/parameterized/1337/beep/42', type: 'string' },
+ 'next.route': { value: '/parameterized/[one]/beep/[two]', type: 'string' },
+ });
+
+ // No child span should share the segment span's name
+ expect(spans.filter(span => !span.is_segment && span.name === segmentSpan.name)).toHaveLength(0);
+ expect(spans.filter(span => getSpanOp(span) === 'http.server')).toEqual([segmentSpan]);
});
test('Will create spans for every server component and metadata generation functions when visiting a page', async ({
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/trace-continuation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/trace-continuation.test.ts
new file mode 100644
index 000000000000..88ebdc16d554
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/trace-continuation.test.ts
@@ -0,0 +1,37 @@
+import { expect, test } from '@playwright/test';
+import { collectStreamedSpans, getRuntime } from '@sentry-internal/test-utils';
+
+test('Continues an incoming trace in the segment of the request', async ({ request }) => {
+ // On Workers the segment is the request span of `withSentry`, on the other runtimes the request span of Next.js.
+ const segmentOrigin = getRuntime() === 'cloudflare' ? 'auto.http.cloudflare' : 'auto';
+ const traceId = crypto.randomUUID().replace(/-/g, '');
+ const parentSpanId = traceId.slice(0, 16);
+
+ const spansPromise = collectStreamedSpans(
+ 'nextjs-16',
+ spans =>
+ spans.some(span => span.trace_id === traceId && span.is_segment) &&
+ spans.some(
+ span => span.trace_id === traceId && span.name === 'render route (app) /parameterized/[one]/beep/[two]',
+ ),
+ );
+
+ await request.get('/parameterized/1337/beep/42', {
+ headers: {
+ 'sentry-trace': `${traceId}-${parentSpanId}-1`,
+ baggage: `sentry-trace_id=${traceId},sentry-sample_rate=1,sentry-sampled=true`,
+ },
+ });
+
+ const spans = await spansPromise;
+ const segmentSpan = spans.find(span => span.trace_id === traceId && span.is_segment)!;
+ const renderSpan = spans.find(
+ span => span.trace_id === traceId && span.name === 'render route (app) /parameterized/[one]/beep/[two]',
+ )!;
+
+ expect(spans.filter(span => span.trace_id === traceId && span.is_segment)).toEqual([segmentSpan]);
+ expect(segmentSpan.parent_span_id).toBe(parentSpanId);
+ expect(segmentSpan.attributes['sentry.origin']?.value).toBe(segmentOrigin);
+ expect(segmentSpan.name).toBe('GET /parameterized/[one]/beep/[two]');
+ expect(renderSpan.attributes['sentry.segment.id']?.value).toBe(segmentSpan.span_id);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/worker-bundle.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/worker-bundle.test.ts
similarity index 95%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/worker-bundle.test.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/tests/worker-bundle.test.ts
index dbd927e2053b..af16d1add37d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/worker-bundle.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/worker-bundle.test.ts
@@ -1,9 +1,12 @@
import { expect, test } from '@playwright/test';
+import { getRuntime } from '@sentry-internal/test-utils';
import * as fs from 'fs';
import { createRequire } from 'module';
import * as path from 'path';
import { isDevMode } from './isDevMode';
+test.skip(getRuntime() !== 'cloudflare', 'Checks the worker bundle of OpenNext');
+
/**
* The orchestrion bundler plugins are build-time-only, and their module-scope side effects break
* on Workers (an unawaited `WebAssembly.compile()` crashed every cold start, issue #22794). The
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/worker.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/worker.ts
similarity index 68%
rename from dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/worker.ts
rename to dev-packages/e2e-tests/test-applications/nextjs-16/worker.ts
index 6718e015a32c..fb5708783e58 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/worker.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/worker.ts
@@ -1,8 +1,8 @@
import * as Sentry from '@sentry/nextjs/cloudflare';
-// @ts-expect-error generated by `opennextjs-cloudflare build`
+// @ts-ignore the file exists only after `opennextjs-cloudflare build`
import handler from './.open-next/worker.js';
-// @ts-expect-error generated by `opennextjs-cloudflare build`
+// @ts-ignore the file exists only after `opennextjs-cloudflare build`
export { DOQueueHandler, DOShardedTagCache, BucketCachePurge } from './.open-next/worker.js';
export default Sentry.withSentry(
@@ -11,6 +11,7 @@ export default Sentry.withSentry(
dsn: env.E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
+ tracePropagationTargets: ['http://localhost:3030/propagation/test-outgoing-fetch/check'],
integrations: [Sentry.vercelAIIntegration()],
}),
handler,
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nextjs-16/wrangler.jsonc
new file mode 100644
index 000000000000..f6bfe6a4ec38
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/wrangler.jsonc
@@ -0,0 +1,36 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "nextjs-16",
+ "main": "./worker.ts",
+ // From 2026-02-19 on, wrangler uses the native `process` of workerd, whose `cwd()` is `/bundle` during a request.
+ // Next.js then misses its router server context and extracts the incoming trace again, see trace-continuation.test.ts.
+ "compatibility_date": "2026-02-19",
+ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
+ "assets": {
+ "binding": "ASSETS",
+ "directory": ".open-next/assets",
+ },
+ "images": {
+ // Enable image optimization
+ // see https://opennext.js.org/cloudflare/howtos/image
+ "binding": "IMAGES",
+ },
+ "services": [
+ {
+ // Self-reference service binding, the service name must match the worker name
+ // see https://opennext.js.org/cloudflare/caching
+ "binding": "WORKER_SELF_REFERENCE",
+ "service": "nextjs-16",
+ },
+ ],
+ "d1_databases": [
+ {
+ "binding": "DB",
+ "database_name": "e2e",
+ "database_id": "00000000-0000-0000-0000-000000000000",
+ },
+ ],
+ "observability": {
+ "enabled": true,
+ },
+}
From 4f9a027988acf007a0ad8d360403221a8069a482 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?=
Date: Thu, 8 Oct 2026 14:28:41 +0300
Subject: [PATCH 27/47] feat(cloudflare): Wrap vinext Worker entries with
`@sentry/nextjs/cloudflare` (#25000)
part of #19514
part of JS-1803
When a Vite build uses vinext and `@sentry/nextjs/cloudflare` resolves
from the Worker entry, the Vite plugin of `@sentry/cloudflare` now wraps
the entry with `withSentry` from `@sentry/nextjs/cloudflare`. Other
builds, and versions of `@sentry/nextjs` without that entry, keep
`@sentry/cloudflare`.
```diff
-import * as __SENTRY__ from '@sentry/cloudflare';
+import * as __SENTRY__ from '@sentry/nextjs/cloudflare';
const __SENTRY_DEFAULT_EXPORT__ = { fetch() { ... } };
export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);
```
## Why
With `withSentry` of `@sentry/cloudflare`, a vinext app on Workers sends
Cloudflare data but no Next.js spans. Now it gets the Next.js spans
without a config change.
## Traces
The same app and requests: vinext on Workers before and after this PR,
and `next start` on Node.js.
| Request | vinext before | vinext after | Next.js on Node.js |
| --- | --- | --- | --- |
| `GET /parameterized/42` | [`GET`, 1
span](https://sentry-sdks.sentry.io/explore/traces/trace/0f98d8ca6ef645e3ae5bb60c039eeb6d/)
| [`GET /parameterized/[id]`, 7
spans](https://sentry-sdks.sentry.io/explore/traces/trace/a26de83affd24b0c8fe7990e567ae8b6/)
| [`GET /parameterized/[id]`, 7
spans](https://sentry-sdks.sentry.io/explore/traces/trace/c24bdbb12b134e80a5417fe9d2a59c56/)
|
| `GET /api/hello` | [`GET`, 1
span](https://sentry-sdks.sentry.io/explore/traces/trace/df5b9f7976c5487daa7004cc9361eeeb/)
| [`GET /api/hello`, 4
spans](https://sentry-sdks.sentry.io/explore/traces/trace/75b4a831b48f4bb1adb13648f5c6419a/)
| [`GET /api/hello`, 4
spans](https://sentry-sdks.sentry.io/explore/traces/trace/ea8990c798144e2db75ee88e8bd6e634/)
|
| `GET /api/throw` (error) | [`GET`, 1
span](https://sentry-sdks.sentry.io/explore/traces/trace/b6389e0368e44b269e5e07ba9823c29a/)
| [`GET /api/throw`, 3
spans](https://sentry-sdks.sentry.io/explore/traces/trace/07caa661217e4f14bb38b182167d08c7/)
| [`GET /api/throw`, 3
spans](https://sentry-sdks.sentry.io/explore/traces/trace/c9a3a73e3c7a44d4b7ff6519c8760a7b/)
|
Co-authored-by: Claude Opus 5.5
---
.../cloudflare/src/vite/autoInstrument.ts | 29 +++++-
packages/cloudflare/src/vite/transform.ts | 4 +-
.../test/vite/autoInstrument.test.ts | 91 ++++++++++++++++++-
3 files changed, 121 insertions(+), 3 deletions(-)
diff --git a/packages/cloudflare/src/vite/autoInstrument.ts b/packages/cloudflare/src/vite/autoInstrument.ts
index 6c7e8f9734fc..c4550f2fa12f 100644
--- a/packages/cloudflare/src/vite/autoInstrument.ts
+++ b/packages/cloudflare/src/vite/autoInstrument.ts
@@ -15,17 +15,39 @@ function normalizePath(path: string): string {
// `.html`, … — sharing the entry's basename must never be treated as the entry.
const JS_EXTENSION_REGEX = /\.[cm]?[jt]sx?$/;
+// Re-exports everything the generated code uses, and adds the Next.js span handling and OpenTelemetry context.
+const NEXTJS_CLOUDFLARE_MODULE = '@sentry/nextjs/cloudflare';
+
+/**
+ * Whether `@sentry/nextjs/cloudflare` resolves from `importer`. Vite throws instead of returning `null` when
+ * `@sentry/nextjs` is installed in a version without that entry.
+ */
+async function resolvesNextjsCloudflare(resolver: ModuleResolver, importer: string): Promise {
+ try {
+ return !!(await resolver.resolve?.(NEXTJS_CLOUDFLARE_MODULE, importer));
+ } catch {
+ return false;
+ }
+}
+
export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPath?: string } = {}) {
let wranglerConfig: WranglerConfig | undefined;
let entryFilePath: string | undefined;
let optionsFn = ENV_FALLBACK_OPTIONS_FN;
let optionsImport: string | undefined;
+ let isVinext = false;
return {
name: 'sentry-cloudflare-auto-instrument',
- configResolved(config: { root: string; logger?: { warn(msg: string): void } }): void {
+ configResolved(config: {
+ root: string;
+ plugins?: readonly { name?: string }[];
+ logger?: { warn(msg: string): void };
+ }): void {
+ isVinext = config.plugins?.some(plugin => plugin.name?.startsWith('vinext:')) ?? false;
+
const result = resolveWranglerConfig(config.root, options.wranglerConfigPath);
if (!result) {
// An explicit path that fails is a misconfiguration worth naming;
@@ -119,6 +141,10 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa
})
: undefined;
+ // A vinext app with `@sentry/nextjs` installed gets `withSentry` of `@sentry/nextjs/cloudflare`.
+ const sentryModule =
+ isVinext && (await resolvesNextjsCloudflare(this, normalizedId)) ? NEXTJS_CLOUDFLARE_MODULE : undefined;
+
// No registration import is injected here: the orchestrion plugin's
// subscribe-injection makes each bundled package self-register its channel
// subscriber on the global marker, so wrapping the entry with `withSentry`
@@ -129,6 +155,7 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa
optionsFn,
optionsImport,
sameWorkerBindings: wranglerConfig.sameWorkerBindings,
+ sentryModule,
});
const wrappedClasses = result?.wrappedClasses ?? new Set();
diff --git a/packages/cloudflare/src/vite/transform.ts b/packages/cloudflare/src/vite/transform.ts
index 1fdf647831c1..3674ed5dd032 100644
--- a/packages/cloudflare/src/vite/transform.ts
+++ b/packages/cloudflare/src/vite/transform.ts
@@ -114,6 +114,8 @@ export interface TransformContext {
optionsFn: string;
/** Import statement prepended when `optionsFn` references a separate module. */
optionsImport?: string;
+ /** Module the generated code imports `withSentry` and the class wrappers from. Defaults to `@sentry/cloudflare`. */
+ sentryModule?: string;
/** @see {@link import('./wranglerConfig').WranglerConfig.sameWorkerBindings} */
sameWorkerBindings?: readonly SameWorkerBinding[];
}
@@ -198,7 +200,7 @@ export function applyAutoInstrumentTransforms(
ms.prepend(buildMergedOptionsDeclaration(sameWorkerBindings, ctx.optionsFn, state));
}
if (ctx.optionsImport) ms.prepend(ctx.optionsImport);
- ms.prepend("import * as __SENTRY__ from '@sentry/cloudflare';\n");
+ ms.prepend(`import * as __SENTRY__ from '${ctx.sentryModule ?? '@sentry/cloudflare'}';\n`);
return {
code: ms.toString(),
diff --git a/packages/cloudflare/test/vite/autoInstrument.test.ts b/packages/cloudflare/test/vite/autoInstrument.test.ts
index 7303e667ee6c..f9654b0d3b5d 100644
--- a/packages/cloudflare/test/vite/autoInstrument.test.ts
+++ b/packages/cloudflare/test/vite/autoInstrument.test.ts
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parse } from 'acorn';
-import { afterEach, describe, expect, it } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import { sentryCloudflareAutoInstrumentPlugin } from '../../src/vite/autoInstrument';
function parseJS(code: string) {
@@ -55,6 +55,95 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => {
expect(result.code).toContain('__SENTRY__.withSentry(');
});
+ it('wraps the entry with `@sentry/nextjs/cloudflare` in a vinext build', async () => {
+ const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
+ const plugin = sentryCloudflareAutoInstrumentPlugin();
+ plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] });
+ const resolve = vi
+ .fn()
+ .mockResolvedValue({ id: join(dir, 'node_modules/@sentry/nextjs/build/esm/cloudflare/index.js') });
+
+ const code = 'export default { fetch() { return new Response("ok"); } };';
+ const result = await plugin.transform.call(
+ { parse: (c: string) => parseJS(c), resolve },
+ code,
+ join(dir, 'src/index.ts'),
+ );
+
+ expect(resolve).toHaveBeenCalledWith('@sentry/nextjs/cloudflare', join(dir, 'src/index.ts'));
+ expect(result.code).toBe(
+ "import * as __SENTRY__ from '@sentry/nextjs/cloudflare';\n" +
+ 'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
+ 'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
+ );
+ });
+
+ it('wraps the entry with `@sentry/cloudflare` when `@sentry/nextjs/cloudflare` resolves outside a vinext build', async () => {
+ const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
+ const plugin = sentryCloudflareAutoInstrumentPlugin();
+ plugin.configResolved({ root: dir, plugins: [{ name: 'vite:react' }] });
+ const resolve = vi
+ .fn()
+ .mockResolvedValue({ id: join(dir, 'node_modules/@sentry/nextjs/build/esm/cloudflare/index.js') });
+
+ const code = 'export default { fetch() { return new Response("ok"); } };';
+ const result = await plugin.transform.call(
+ { parse: (c: string) => parseJS(c), resolve },
+ code,
+ join(dir, 'src/index.ts'),
+ );
+
+ expect(result.code).toBe(
+ "import * as __SENTRY__ from '@sentry/cloudflare';\n" +
+ 'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
+ 'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
+ );
+ });
+
+ it('wraps the entry with `@sentry/cloudflare` in a vinext build without `@sentry/nextjs`', async () => {
+ const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
+ const plugin = sentryCloudflareAutoInstrumentPlugin();
+ plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] });
+ const resolve = vi.fn().mockResolvedValue(null);
+
+ const code = 'export default { fetch() { return new Response("ok"); } };';
+ const result = await plugin.transform.call(
+ { parse: (c: string) => parseJS(c), resolve },
+ code,
+ join(dir, 'src/index.ts'),
+ );
+
+ expect(result.code).toBe(
+ "import * as __SENTRY__ from '@sentry/cloudflare';\n" +
+ 'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
+ 'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
+ );
+ });
+
+ it('wraps the entry with `@sentry/cloudflare` in a vinext build with an `@sentry/nextjs` without the `./cloudflare` entry', async () => {
+ const dir = writeTempDir({ 'wrangler.toml': 'main = "src/index.ts"' });
+ const plugin = sentryCloudflareAutoInstrumentPlugin();
+ plugin.configResolved({ root: dir, plugins: [{ name: 'vinext:config' }] });
+ const resolve = vi
+ .fn()
+ .mockRejectedValue(
+ new Error('"./cloudflare" is not exported under the conditions ["workerd"] from package @sentry/nextjs'),
+ );
+
+ const code = 'export default { fetch() { return new Response("ok"); } };';
+ const result = await plugin.transform.call(
+ { parse: (c: string) => parseJS(c), resolve },
+ code,
+ join(dir, 'src/index.ts'),
+ );
+
+ expect(result.code).toBe(
+ "import * as __SENTRY__ from '@sentry/cloudflare';\n" +
+ 'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
+ 'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
+ );
+ });
+
it('leaves an already-manually-wrapped entry untouched', async () => {
const { transform: tx, entryPath } = createPlugin('main = "src/index.ts"');
From b1638a0c92c7020e580739c1367e4fd7d581fa1d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 8 Oct 2026 13:44:42 +0200
Subject: [PATCH 28/47] feat(deps): Bump next from 16.2.11 to 16.3.8 in
/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps (#25170)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [next](https://github.com/vercel/next.js) from 16.2.11 to 16.3.8.
Release notes
Sourced from next's
releases.
v16.3.8
This release contains security fixes for the following
advisories:
High:
Medium:
Low:
v16.3.7
[!NOTE]
This release is backporting bug fixes. It does not
include all pending features/changes on canary.
Core Changes
- turbo-tasks-backend: fix strongly consistent read hanging on a
canceled task (#98931)
Credits
Huge thanks to @lukesandberg
for helping!
v16.3.6
This release contains a security fix for GHSA-vcvr-r3jv-pc5j:
Remote Code Execution in next/og ImageResponse
v16.3.5
The following bug fixes have been backported. It does not include all
pending features/changes on canary.
- next/image: Skip 0-byte entries when initializing disk LRU cache (#98185)
- next/image: Reject empty images when reading/writing to the disk
cache (#98186)
- Emit whole-app server NFTs when
output: 'standalone' is
used with an adapter (#98167)
- Add CSP nonce to script tags of loading and template files (#98403)
- Fix
use cache prerender signal retention (#98448)
v16.3.4
Follow-up release to v16.3.3
re-enabling AVIF Image Optimization (#97949).
The following bug fixes have been backported. It does
not include all pending features/changes on canary.
- testmode: Fix infinite recursion in testmode passthrough fetch (#97691)
- Fix build error when aliasing typescript to
@typescript/typescript6 (#97997)
- Fix unset crossOrigin in Turbopack manifests (#97930)
Credits
... (truncated)
Commits
b0fad0d
v16.3.8
719e4c6
[lts-active] Scope response cache keys to their source route (#218)
e92db45
[lts-active] Fix metadata propagation for deduplicated nested caches (#223)
40c2ba9
[lts-active] Match Next data paths case-sensitively (#196)
2d9f50a
[lts-active] Fix MCP middleware DNS rebinding (#213)
bd9214f
[lts-active] Fix draft mode leaks through cross-request 'use
cache' dedupli...
8db4a62
[lts-active][webpack] Ensure dynamicParams is respected in
`opengraph-image...
e002ad6
[lts-active] fix(next/image): Pin DNS resolution when fetching external
image...
4c20699
v16.3.7
2521aec
[backport] turbo-tasks-backend: fix strongly consistent read hanging on
a can...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.../e2e-tests/test-applications/nextjs-sourcemaps/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json
index ee79cb9ff340..ddfd68285ec3 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
- "next": "16.2.11",
+ "next": "16.3.8",
"react": "19.1.0",
"react-dom": "19.1.0",
"typescript": "~5.0.0"
From b6616ce3808270295027e6eee6328e250d7e63ae Mon Sep 17 00:00:00 2001
From: Nicolas Hrubec
Date: Thu, 8 Oct 2026 13:44:58 +0200
Subject: [PATCH 29/47] test(node): Port AMQP integration tests to span
streaming (#25158)
Port AMQP tests to span streaming.
Part of #24137
---------
Co-authored-by: GPT-6
---
.../amqplib/instrument-span-streaming.mjs | 10 -
.../suites/tracing/amqplib/instrument.mjs | 1 -
.../suites/tracing/amqplib/test.ts | 173 +++++++-----------
3 files changed, 65 insertions(+), 119 deletions(-)
delete mode 100644 dev-packages/node-integration-tests/suites/tracing/amqplib/instrument-span-streaming.mjs
diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument-span-streaming.mjs
deleted file mode 100644
index 54c513068360..000000000000
--- a/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument-span-streaming.mjs
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as Sentry from '@sentry/node';
-import { loggingTransport } from '@sentry-internal/node-integration-tests';
-
-Sentry.init({
- traceLifecycle: 'stream',
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- release: '1.0',
- tracesSampleRate: 1.0,
- transport: loggingTransport,
-});
diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument.mjs
index 170ad6f6a702..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/instrument.mjs
@@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
- traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
diff --git a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
index fade84b952a4..9e162e07490c 100644
--- a/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/amqplib/test.ts
@@ -1,4 +1,19 @@
-import type { TransactionEvent } from '@sentry/core';
+import {
+ MESSAGING_DESTINATION_NAME,
+ MESSAGING_OPERATION_NAME,
+ MESSAGING_OPERATION_TYPE,
+ MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY,
+ MESSAGING_SYSTEM,
+ NETWORK_PROTOCOL_NAME,
+ NETWORK_PROTOCOL_VERSION,
+ SENTRY_KIND,
+ SENTRY_OP,
+ SENTRY_ORIGIN,
+ SERVER_ADDRESS,
+ SERVER_PORT,
+ URL_FULL,
+} from '@sentry/conventions/attributes';
+import { QUEUE_PROCESS, QUEUE_PUBLISH } from '@sentry/conventions/op';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner';
@@ -8,37 +23,35 @@ import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose
// default (empty) exchange with the queue name as the routing key.
const expectedProducerSpan = (routingKey: string) =>
expect.objectContaining({
- op: 'queue.publish',
- data: expect.objectContaining({
- 'messaging.system': 'rabbitmq',
- 'messaging.operation.name': 'send',
- 'messaging.operation.type': 'send',
- 'messaging.destination.name': routingKey,
- 'messaging.rabbitmq.destination.routing_key': routingKey,
- 'network.protocol.name': 'AMQP',
- 'network.protocol.version': '0.9.1',
- 'server.address': 'localhost',
- 'server.port': 5672,
- 'url.full': 'amqp://sentry:***@localhost:5672/',
- 'sentry.kind': 'producer',
- 'sentry.op': 'queue.publish',
- 'sentry.origin': 'auto.amqplib.publisher',
+ attributes: expect.objectContaining({
+ [MESSAGING_SYSTEM]: { type: 'string', value: 'rabbitmq' },
+ [MESSAGING_OPERATION_NAME]: { type: 'string', value: 'send' },
+ [MESSAGING_OPERATION_TYPE]: { type: 'string', value: 'send' },
+ [MESSAGING_DESTINATION_NAME]: { type: 'string', value: routingKey },
+ [MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]: { type: 'string', value: routingKey },
+ [NETWORK_PROTOCOL_NAME]: { type: 'string', value: 'AMQP' },
+ [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: '0.9.1' },
+ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' },
+ [SERVER_PORT]: { type: 'integer', value: 5672 },
+ [URL_FULL]: { type: 'string', value: 'amqp://sentry:***@localhost:5672/' },
+ [SENTRY_KIND]: { type: 'string', value: 'producer' },
+ [SENTRY_OP]: { type: 'string', value: QUEUE_PUBLISH },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.amqplib.publisher' },
}),
status: 'ok',
});
const EXPECTED_MESSAGE_SPAN_CONSUMER = expect.objectContaining({
- op: 'queue.process',
- data: expect.objectContaining({
- 'messaging.system': 'rabbitmq',
+ attributes: expect.objectContaining({
+ [MESSAGING_SYSTEM]: { type: 'string', value: 'rabbitmq' },
// The delivery carries the default exchange (''), so the routing key is the destination.
- 'messaging.destination.name': 'queue1',
- 'messaging.rabbitmq.destination.routing_key': 'queue1',
- 'messaging.operation.name': 'process',
- 'messaging.operation.type': 'process',
- 'sentry.kind': 'consumer',
- 'sentry.op': 'queue.process',
- 'sentry.origin': 'auto.amqplib.consumer',
+ [MESSAGING_DESTINATION_NAME]: { type: 'string', value: 'queue1' },
+ [MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]: { type: 'string', value: 'queue1' },
+ [MESSAGING_OPERATION_NAME]: { type: 'string', value: 'process' },
+ [MESSAGING_OPERATION_TYPE]: { type: 'string', value: 'process' },
+ [SENTRY_KIND]: { type: 'string', value: 'consumer' },
+ [SENTRY_OP]: { type: 'string', value: QUEUE_PROCESS },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.amqplib.consumer' },
}),
status: 'ok',
});
@@ -59,40 +72,27 @@ describeWithDockerCompose('amqplib auto-instrumentation', { workingDirectory: [_
'instrument.mjs',
(createTestRunner, test) => {
test('should be able to send and receive messages', { timeout: 60_000 }, async () => {
- // The producer ('root span') and consumer ('queue1 process') transactions can
- // arrive in any order, so we collect them and assert after both are received.
- const receivedTransactions: TransactionEvent[] = [];
-
await createTestRunner()
.expect({
- transaction: (transaction: TransactionEvent) => {
- receivedTransactions.push(transaction);
- },
- })
- .expect({
- transaction: (transaction: TransactionEvent) => {
- receivedTransactions.push(transaction);
-
- // The producer span is a child of the manually-started 'root span' transaction, so we
- // identify it by its origin rather than by transaction name. The consumer span is its
- // own transaction, identified by the origin on its trace context.
- const producer = receivedTransactions.find(t =>
- t.spans?.some(s => s.data?.['sentry.origin'] === 'auto.amqplib.publisher'),
- );
- const consumer = receivedTransactions.find(
- t => t.contexts?.trace?.data?.['sentry.origin'] === 'auto.amqplib.consumer',
+ span: container => {
+ const producer = container.items.find(span => span.is_segment && span.name === 'root span');
+ const consumer = container.items.find(
+ t => t.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.consumer',
);
expect(producer).toBeDefined();
expect(consumer).toBeDefined();
- expect(producer!.transaction).toBe('root span');
- expect(consumer!.transaction).toBe('queue1 process');
+ expect(producer!.name).toBe('root span');
+ expect(consumer!.name).toBe('process queue1');
- const producerSpan = producer!.spans?.find(s => s.data?.['sentry.origin'] === 'auto.amqplib.publisher');
+ const producerSpan = container.items.find(
+ s => s.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.publisher',
+ );
+ expect(producerSpan?.name).toBe('send queue1');
expect(producerSpan).toMatchObject(expectedProducerSpan('queue1'));
- expect(consumer!.contexts?.trace).toMatchObject(EXPECTED_MESSAGE_SPAN_CONSUMER);
+ expect(consumer!).toMatchObject(EXPECTED_MESSAGE_SPAN_CONSUMER);
},
})
.start()
@@ -108,35 +108,23 @@ describeWithDockerCompose('amqplib auto-instrumentation', { workingDirectory: [_
'instrument.mjs',
(createTestRunner, test) => {
test('marks the consumer span as errored when the message is rejected', { timeout: 60_000 }, async () => {
- // The error scenario emits the producer ('root span') and the rejected consumer
- // ('queue1 process') transactions in any order, so we collect both and assert on the consumer.
- const receivedTransactions: TransactionEvent[] = [];
-
await createTestRunner()
.expect({
- transaction: (transaction: TransactionEvent) => {
- receivedTransactions.push(transaction);
- },
- })
- .expect({
- transaction: (transaction: TransactionEvent) => {
- receivedTransactions.push(transaction);
-
- const consumer = receivedTransactions.find(
- t => t.contexts?.trace?.data?.['sentry.origin'] === 'auto.amqplib.consumer',
+ span: container => {
+ const consumer = container.items.find(
+ t => t.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.consumer',
);
expect(consumer).toBeDefined();
- expect(consumer!.transaction).toBe('queue-error process');
- expect(consumer!.contexts?.trace).toMatchObject(
+ expect(consumer!.name).toBe('process queue-error');
+ expect(consumer!).toMatchObject(
expect.objectContaining({
- op: 'queue.process',
- status: 'internal_error',
- data: expect.objectContaining({
- 'messaging.system': 'rabbitmq',
- 'sentry.kind': 'consumer',
- 'sentry.op': 'queue.process',
- 'sentry.origin': 'auto.amqplib.consumer',
+ status: 'error',
+ attributes: expect.objectContaining({
+ [MESSAGING_SYSTEM]: { type: 'string', value: 'rabbitmq' },
+ [SENTRY_KIND]: { type: 'string', value: 'consumer' },
+ [SENTRY_OP]: { type: 'string', value: QUEUE_PROCESS },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.amqplib.consumer' },
}),
}),
);
@@ -160,11 +148,11 @@ describeWithDockerCompose('amqplib auto-instrumentation', { workingDirectory: [_
async () => {
await createTestRunner()
.expect({
- transaction: (transaction: TransactionEvent) => {
- expect(transaction.transaction).toBe('root span');
+ span: container => {
+ expect(container.items.find(span => span.is_segment)?.name).toBe('root span');
- const producerSpans = transaction.spans?.filter(
- s => s.data?.['sentry.origin'] === 'auto.amqplib.publisher',
+ const producerSpans = container.items.filter(
+ s => s.attributes[SENTRY_ORIGIN]?.value === 'auto.amqplib.publisher',
);
// The confirm channel internally calls the base publish; the instrumentation must not
@@ -181,35 +169,4 @@ describeWithDockerCompose('amqplib auto-instrumentation', { workingDirectory: [_
{ additionalDependencies },
);
});
-
- createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createTestRunner, test) => {
- test('names streamed spans after the messaging conventions', { timeout: 60_000 }, async () => {
- await createTestRunner()
- .ignore('event')
- .expect({
- span: container => {
- // `sendToQueue` publishes to the default exchange, which has no name. Its routing key is the
- // queue name, so it is the destination rather than per-message data.
- for (const origin of ['auto.amqplib.publisher', 'auto.amqplib.consumer']) {
- const span = container.items.find(item => item.attributes['sentry.origin']?.value === origin);
- expect(span).toBeDefined();
- expect(span!.attributes['messaging.destination.name']?.value).toBe('queue1');
- expect(span!.attributes['messaging.rabbitmq.destination.routing_key']?.value).toBe('queue1');
- }
-
- const producerSpan = container.items.find(
- span => span.attributes['sentry.origin']?.value === 'auto.amqplib.publisher',
- );
- expect(producerSpan!.name).toBe('send queue1');
-
- const consumerSpan = container.items.find(
- span => span.attributes['sentry.origin']?.value === 'auto.amqplib.consumer',
- );
- expect(consumerSpan!.name).toBe('process queue1');
- },
- })
- .start()
- .completed();
- });
- });
});
From 8acc0c8786c93dc94ce831a3076053cc2838a949 Mon Sep 17 00:00:00 2001
From: Nicolas Hrubec
Date: Thu, 8 Oct 2026 13:46:09 +0200
Subject: [PATCH 30/47] test(node): Port pool and DataLoader integration tests
to span streaming (#25156)
Port generic-pool and dataloader tests to span streaming.
Part of #24137
---------
Co-authored-by: GPT-6
---
.../suites/tracing/dataloader/instrument.mjs | 1 -
.../suites/tracing/dataloader/test.ts | 135 ++++++++----------
.../tracing/genericPool-v2/instrument.mjs | 1 -
.../suites/tracing/genericPool-v2/test.ts | 48 ++++---
.../suites/tracing/genericPool/instrument.mjs | 1 -
.../suites/tracing/genericPool/test.ts | 49 ++++---
6 files changed, 106 insertions(+), 129 deletions(-)
diff --git a/dev-packages/node-integration-tests/suites/tracing/dataloader/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/dataloader/instrument.mjs
index 22bf57f14364..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/dataloader/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/dataloader/instrument.mjs
@@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
- traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
diff --git a/dev-packages/node-integration-tests/suites/tracing/dataloader/test.ts b/dev-packages/node-integration-tests/suites/tracing/dataloader/test.ts
index b49004cf02a9..620a4311d083 100644
--- a/dev-packages/node-integration-tests/suites/tracing/dataloader/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/dataloader/test.ts
@@ -1,4 +1,13 @@
-import type { SerializedStreamedSpanContainer } from '@sentry/core';
+import {
+ CACHE_KEY,
+ CACHE_OPERATION,
+ DB_COLLECTION_NAME,
+ DB_OPERATION_NAME,
+ SENTRY_KIND,
+ SENTRY_OP,
+ SENTRY_ORIGIN,
+} from '@sentry/conventions/attributes';
+import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
@@ -6,8 +15,7 @@ import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runn
// enabled (via INJECT_ORCHESTRION) the OTel `Dataloader` integration is swapped for the
// diagnostics-channel one, which stamps a different origin.
const ORIGIN = 'auto.db.dataloader';
-const CACHE_GET_OP = 'cache.get';
-const CACHE_MUTATION_OPS = { prime: 'cache.put', clear: 'cache.remove', clearAll: 'cache.remove' } as const;
+const CACHE_MUTATION_OPS = { prime: CACHE_PUT, clear: CACHE_REMOVE, clearAll: CACHE_REMOVE } as const;
describe('dataloader auto-instrumentation', () => {
afterAll(() => {
@@ -18,30 +26,28 @@ describe('dataloader auto-instrumentation', () => {
test('instruments load, loadMany, batch, prime, clear and clearAll', async () => {
const runner = createRunner()
.expect({
- transaction: event => {
- expect(event.transaction).toBe('GET /load');
+ span: container => {
+ expect(container.items.find(span => span.is_segment)?.name).toBe('GET /load');
- const spans = event.spans || [];
+ const spans = container.items;
- const loadSpan = spans.find(span => span.description === 'dataloader.load');
+ const loadSpan = spans.find(span => span.attributes[DB_OPERATION_NAME]?.value === 'load');
expect(loadSpan).toBeDefined();
- expect(loadSpan?.op).toBe(CACHE_GET_OP);
- expect(loadSpan?.origin).toBe(ORIGIN);
+ expect(loadSpan?.attributes[SENTRY_OP]?.value).toBe(CACHE_GET);
+ expect(loadSpan?.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN);
expect(loadSpan?.status).toBe('ok');
- expect(loadSpan?.data?.['sentry.origin']).toBe(ORIGIN);
- expect(loadSpan?.data?.['sentry.op']).toBe(CACHE_GET_OP);
- expect(loadSpan?.data?.['cache.key']).toEqual(['user-1']);
- expect(loadSpan?.data?.['db.operation.name']).toBe('load');
+ expect(loadSpan?.attributes[CACHE_KEY]?.value).toEqual(['user-1']);
+ expect(loadSpan?.attributes[DB_OPERATION_NAME]?.value).toBe('load');
// A direct operation is a client call; the deferred `batch` below gets no kind
- expect(loadSpan?.data?.['sentry.kind']).toBe('client');
+ expect(loadSpan?.attributes[SENTRY_KIND]?.value).toBe('client');
- const batchSpan = spans.find(span => span.description === 'dataloader.batch');
+ const batchSpan = spans.find(span => span.attributes[DB_OPERATION_NAME]?.value === 'batch');
expect(batchSpan).toBeDefined();
- expect(batchSpan?.op).toBe(CACHE_GET_OP);
- expect(batchSpan?.origin).toBe(ORIGIN);
+ expect(batchSpan?.attributes[SENTRY_OP]?.value).toBe(CACHE_GET);
+ expect(batchSpan?.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN);
expect(batchSpan?.status).toBe('ok');
- expect(batchSpan?.data?.['cache.key']).toEqual(['user-1']);
- expect(batchSpan?.data?.['sentry.kind']).toBeUndefined();
+ expect(batchSpan?.attributes[CACHE_KEY]?.value).toEqual(['user-1']);
+ expect(batchSpan?.attributes[SENTRY_KIND]?.value).toBeUndefined();
// The batch span links back to the load span that triggered it
expect(batchSpan?.links).toEqual([
@@ -54,57 +60,62 @@ describe('dataloader auto-instrumentation', () => {
// Locks down the async behavior: `load` encloses the deferred `batch` span
expect(batchSpan?.parent_span_id).toBe(loadSpan?.span_id);
expect(loadSpan?.start_timestamp).toBeLessThanOrEqual(batchSpan?.start_timestamp ?? 0);
- expect(loadSpan?.timestamp).toBeGreaterThanOrEqual(batchSpan?.timestamp ?? 0);
+ expect(loadSpan?.end_timestamp).toBeGreaterThanOrEqual(batchSpan?.end_timestamp ?? 0);
},
})
.expect({
- transaction: event => {
- expect(event.transaction).toBe('GET /load-many');
+ span: container => {
+ expect(container.items.find(span => span.is_segment)?.name).toBe('GET /load-many');
- const loadManySpan = (event.spans || []).find(span => span.description === 'dataloader.loadMany');
+ const loadManySpan = container.items.find(span => span.attributes[DB_OPERATION_NAME]?.value === 'loadMany');
expect(loadManySpan).toBeDefined();
- expect(loadManySpan?.op).toBe(CACHE_GET_OP);
- expect(loadManySpan?.origin).toBe(ORIGIN);
+ expect(loadManySpan?.attributes[SENTRY_OP]?.value).toBe(CACHE_GET);
+ expect(loadManySpan?.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN);
expect(loadManySpan?.status).toBe('ok');
- expect(loadManySpan?.data?.['sentry.origin']).toBe(ORIGIN);
- expect(loadManySpan?.data?.['sentry.op']).toBe(CACHE_GET_OP);
- expect(loadManySpan?.data?.['cache.key']).toEqual(['user-1', 'user-2']);
+ expect(loadManySpan?.attributes[CACHE_KEY]?.value).toEqual(['user-1', 'user-2']);
},
})
.expect({
- transaction: event => {
- expect(event.transaction).toBe('GET /cache-ops');
+ span: container => {
+ expect(container.items.find(span => span.is_segment)?.name).toBe('GET /cache-ops');
- const spans = event.spans || [];
+ const spans = container.items;
// prime writes to the cache, clear/clearAll remove from it
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
- const span = spans.find(s => s.description === `dataloader.${operation}`);
+ const span = spans.find(s => s.attributes[DB_OPERATION_NAME]?.value === operation);
expect(span, `expected a dataloader.${operation} span`).toBeDefined();
- expect(span?.origin).toBe(ORIGIN);
+ expect(span?.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN);
expect(span?.status).toBe('ok');
- expect(span?.op).toBe(op);
- expect(span?.data?.['sentry.origin']).toBe(ORIGIN);
- expect(span?.data?.['db.operation.name']).toBe(operation);
+ expect(span?.name).toBe(op);
+ expect(span?.attributes[SENTRY_OP]?.value).toBe(op);
+ expect(span?.attributes[DB_OPERATION_NAME]?.value).toBe(operation);
}
// `clearAll` takes no key, the other two act on a single key
- expect(spans.find(s => s.description === 'dataloader.prime')?.data?.['cache.key']).toEqual(['user-1']);
- expect(spans.find(s => s.description === 'dataloader.clear')?.data?.['cache.key']).toEqual(['user-1']);
- expect(spans.find(s => s.description === 'dataloader.clearAll')?.data?.['cache.key']).toBeUndefined();
+ expect(
+ spans.find(s => s.attributes[DB_OPERATION_NAME]?.value === 'prime')?.attributes[CACHE_KEY]?.value,
+ ).toEqual(['user-1']);
+ expect(
+ spans.find(s => s.attributes[DB_OPERATION_NAME]?.value === 'clear')?.attributes[CACHE_KEY]?.value,
+ ).toEqual(['user-1']);
+ expect(
+ spans.find(s => s.attributes[DB_OPERATION_NAME]?.value === 'clearAll')?.attributes[CACHE_KEY]?.value,
+ ).toBeUndefined();
},
})
.expect({
- transaction: event => {
- expect(event.transaction).toBe('GET /named');
+ span: container => {
+ expect(container.items.find(span => span.is_segment)?.name).toBe('GET /named');
- // A named dataloader includes its name in the span description
- const namedLoadSpan = (event.spans || []).find(span => span.description === 'dataloader.load usersLoader');
+ const namedLoadSpan = container.items.find(span => span.attributes[DB_OPERATION_NAME]?.value === 'load');
expect(namedLoadSpan).toBeDefined();
- expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
- expect(namedLoadSpan?.origin).toBe(ORIGIN);
+ expect(namedLoadSpan?.attributes[SENTRY_OP]?.value).toBe(CACHE_GET);
+ expect(namedLoadSpan?.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
- expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
+ expect(namedLoadSpan?.name).toBe(CACHE_GET);
+ expect(namedLoadSpan?.attributes[CACHE_OPERATION]?.value).toBe('get');
+ expect(namedLoadSpan?.attributes[DB_COLLECTION_NAME]?.value).toBe('usersLoader');
},
})
.start();
@@ -115,37 +126,5 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);
-
- test('names spans after the cache operation when streamed', async () => {
- const runner = createRunner()
- .withEnv({ STREAMED: 'true' })
- .expect({
- span: (container: SerializedStreamedSpanContainer) => {
- const namedLoadSpan = container.items.find(
- span => span.attributes?.['db.operation.name']?.value === 'load',
- );
- expect(namedLoadSpan?.name).toBe('cache.get');
- expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
- expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
- // The loader name is no longer part of the span name, it moved to `db.collection.name`.
- expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
- },
- })
- .expect({
- span: (container: SerializedStreamedSpanContainer) => {
- for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
- const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
- expect(span, `expected a ${operation} span`).toBeDefined();
- expect(span?.name).toBe(op);
- expect(span?.attributes?.['sentry.op']?.value).toBe(op);
- }
- },
- })
- .start();
-
- await runner.makeRequest('get', '/named');
- await runner.makeRequest('get', '/cache-ops');
- await runner.completed();
- }, 30_000);
});
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/instrument.mjs
index 170ad6f6a702..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/instrument.mjs
@@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
- traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
diff --git a/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/test.ts b/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/test.ts
index 5f744edbbf3f..08f30f1545ef 100644
--- a/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/genericPool-v2/test.ts
@@ -1,3 +1,5 @@
+import { ERROR_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
+import { DB } from '@sentry/conventions/op';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
@@ -12,32 +14,30 @@ describe('genericPool v2 auto instrumentation', () => {
'instrument.mjs',
(createRunner, test) => {
test('should auto-instrument `generic-pool` v2 when calling pool.acquire()', async () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
+ const EXPECTED_SPANS = {
+ items: expect.arrayContaining([
+ expect.objectContaining({ name: 'Test Transaction', is_segment: true }),
expect.objectContaining({
- description: 'generic-pool.acquire',
- op: 'db',
- origin: 'auto.db.generic_pool',
- data: expect.objectContaining({
- 'sentry.origin': 'auto.db.generic_pool',
+ name: 'generic-pool.acquire',
+ attributes: expect.objectContaining({
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.generic_pool' },
}),
status: 'ok',
}),
expect.objectContaining({
- description: 'generic-pool.acquire',
- op: 'db',
- origin: 'auto.db.generic_pool',
- data: expect.objectContaining({
- 'sentry.origin': 'auto.db.generic_pool',
+ name: 'generic-pool.acquire',
+ attributes: expect.objectContaining({
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.generic_pool' },
}),
status: 'ok',
}),
]),
};
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
+ await createRunner().expect({ span: EXPECTED_SPANS }).start().completed();
});
},
{ additionalDependencies: { 'generic-pool': '^2.5.0' } },
@@ -49,20 +49,22 @@ describe('genericPool v2 auto instrumentation', () => {
'instrument.mjs',
(createRunner, test) => {
test('marks the `generic-pool.acquire` span as errored when acquiring fails', async () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
+ const EXPECTED_SPANS = {
+ items: expect.arrayContaining([
+ expect.objectContaining({ name: 'Test Transaction', is_segment: true }),
expect.objectContaining({
- description: 'generic-pool.acquire',
- op: 'db',
- origin: 'auto.db.generic_pool',
- data: expect.objectContaining({ 'sentry.origin': 'auto.db.generic_pool', 'error.type': 'Error' }),
- status: 'internal_error',
+ name: 'generic-pool.acquire',
+ attributes: expect.objectContaining({
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.generic_pool' },
+ [ERROR_TYPE]: { type: 'string', value: 'Error' },
+ }),
+ status: 'error',
}),
]),
};
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
+ await createRunner().expect({ span: EXPECTED_SPANS }).start().completed();
});
},
{ additionalDependencies: { 'generic-pool': '^2.5.0' } },
diff --git a/dev-packages/node-integration-tests/suites/tracing/genericPool/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/genericPool/instrument.mjs
index 170ad6f6a702..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/genericPool/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/genericPool/instrument.mjs
@@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
- traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
diff --git a/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts b/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts
index 2c607666691a..bce47d5f779f 100644
--- a/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts
@@ -1,3 +1,5 @@
+import { ERROR_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
+import { DB } from '@sentry/conventions/op';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
@@ -8,54 +10,51 @@ describe('genericPool auto instrumentation', () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should auto-instrument `genericPool` package when calling pool.require()', async () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
+ const EXPECTED_SPANS = {
+ items: expect.arrayContaining([
+ expect.objectContaining({ name: 'Test Transaction', is_segment: true }),
expect.objectContaining({
- description: 'generic-pool.acquire',
- op: 'db',
- origin: 'auto.db.generic_pool',
- data: expect.objectContaining({
- 'sentry.origin': 'auto.db.generic_pool',
+ name: 'generic-pool.acquire',
+ attributes: expect.objectContaining({
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.generic_pool' },
}),
status: 'ok',
}),
expect.objectContaining({
- description: 'generic-pool.acquire',
- op: 'db',
- origin: 'auto.db.generic_pool',
- data: expect.objectContaining({
- 'sentry.origin': 'auto.db.generic_pool',
+ name: 'generic-pool.acquire',
+ attributes: expect.objectContaining({
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.generic_pool' },
}),
status: 'ok',
}),
]),
};
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
+ await createRunner().expect({ span: EXPECTED_SPANS }).start().completed();
});
});
createEsmAndCjsTests(__dirname, 'scenario-error.mjs', 'instrument.mjs', (createRunner, test) => {
test('marks the `generic-pool.acquire` span as errored when acquiring fails', async () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
+ const EXPECTED_SPANS = {
+ items: expect.arrayContaining([
+ expect.objectContaining({ name: 'Test Transaction', is_segment: true }),
expect.objectContaining({
- description: 'generic-pool.acquire',
- op: 'db',
- origin: 'auto.db.generic_pool',
- data: expect.objectContaining({
- 'sentry.origin': 'auto.db.generic_pool',
- 'error.type': 'TimeoutError',
+ name: 'generic-pool.acquire',
+ attributes: expect.objectContaining({
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.generic_pool' },
+ [ERROR_TYPE]: { type: 'string', value: 'TimeoutError' },
}),
- status: 'internal_error',
+ status: 'error',
}),
]),
};
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
+ await createRunner().expect({ span: EXPECTED_SPANS }).start().completed();
});
});
});
From cf24f0cb7428f5978105f560dfe22f886a16196d Mon Sep 17 00:00:00 2001
From: Nicolas Hrubec
Date: Thu, 8 Oct 2026 13:46:29 +0200
Subject: [PATCH 31/47] test(node): Port Knex integration tests to span
streaming (#25154)
Port the Knex MySQL2 and Postgres tests to default span streaming,
preserving their assertions and folding in the existing query-name
checks.
Part of #24137
---------
Co-authored-by: GPT-6
---
.../knex/mysql2/instrument-span-streaming.mjs | 10 -
.../suites/tracing/knex/mysql2/instrument.mjs | 1 -
.../suites/tracing/knex/mysql2/test.ts | 153 +++++++--------
.../knex/pg/instrument-span-streaming.mjs | 10 -
.../suites/tracing/knex/pg/instrument.mjs | 1 -
.../suites/tracing/knex/pg/test.ts | 181 ++++++++----------
6 files changed, 159 insertions(+), 197 deletions(-)
delete mode 100644 dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument-span-streaming.mjs
delete mode 100644 dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument-span-streaming.mjs
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument-span-streaming.mjs
deleted file mode 100644
index 53b9511a21f0..000000000000
--- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument-span-streaming.mjs
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as Sentry from '@sentry/node';
-import { loggingTransport } from '@sentry-internal/node-integration-tests';
-
-Sentry.init({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- release: '1.0',
- tracesSampleRate: 1.0,
- transport: loggingTransport,
- traceLifecycle: 'stream',
-});
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument.mjs
index 170ad6f6a702..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/instrument.mjs
@@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
- traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts
index 98cb1127906b..ec3947f54f4a 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/mysql2/test.ts
@@ -1,3 +1,16 @@
+import {
+ DB_NAMESPACE,
+ DB_OPERATION_NAME,
+ DB_QUERY_SUMMARY,
+ DB_QUERY_TEXT,
+ DB_SYSTEM_NAME,
+ DB_USER,
+ SENTRY_OP,
+ SENTRY_ORIGIN,
+ SERVER_ADDRESS,
+ SERVER_PORT,
+} from '@sentry/conventions/attributes';
+import { DB } from '@sentry/conventions/op';
import { describe, expect } from 'vitest';
import { createEsmAndCjsTests, describeWithDockerCompose } from '../../../../utils/runner';
@@ -9,93 +22,75 @@ describeWithDockerCompose('knex auto instrumentation', { workingDirectory: [__di
describe('with `mysql2` client', () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should auto-instrument `knex` package', { timeout: 60_000 }, async () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.system.name': 'mysql2',
- 'db.namespace': 'tests',
- 'db.user': 'root',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- 'server.address': 'localhost',
- 'server.port': 3307,
- }),
- status: 'ok',
- description:
- 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(?) not null default CURRENT_TIMESTAMP(?), `email` text not null, `name` text not null)',
- origin: ORIGIN,
+ const EXPECTED_SPANS = [
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql2' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [DB_USER]: { type: 'string', value: 'root' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' },
+ [SERVER_PORT]: { type: 'integer', value: 3307 },
+ [DB_QUERY_TEXT]: {
+ type: 'string',
+ value:
+ 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(?) not null default CURRENT_TIMESTAMP(?), `email` text not null, `name` text not null)',
+ },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'create table `User`' },
}),
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.system.name': 'mysql2',
- 'db.namespace': 'tests',
- 'db.user': 'root',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- 'server.address': 'localhost',
- 'server.port': 3307,
- }),
- status: 'ok',
- description: 'insert into `User` (`email`, `name`) values (?, ?)',
- origin: ORIGIN,
+ status: 'ok',
+ name: 'create table `User`',
+ }),
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql2' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [DB_USER]: { type: 'string', value: 'root' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' },
+ [SERVER_PORT]: { type: 'integer', value: 3307 },
+ [DB_QUERY_TEXT]: { type: 'string', value: 'insert into `User` (`email`, `name`) values (?, ?)' },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'insert `User`' },
}),
+ status: 'ok',
+ name: 'insert `User`',
+ }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.operation.name': 'select',
- 'db.sql.table': 'User',
- 'db.system.name': 'mysql2',
- 'db.namespace': 'tests',
- 'db.query.text': 'select * from `User`',
- 'db.user': 'root',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- }),
- status: 'ok',
- description: 'select * from `User`',
- origin: ORIGIN,
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_OPERATION_NAME]: { type: 'string', value: 'select' },
+ 'db.sql.table': { type: 'string', value: 'User' },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'mysql2' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [DB_QUERY_TEXT]: { type: 'string', value: 'select * from `User`' },
+ [DB_USER]: { type: 'string', value: 'root' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'select `User`' },
}),
- ]),
- };
-
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
- });
- });
+ status: 'ok',
+ name: 'select `User`',
+ }),
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ [DB_QUERY_TEXT]: { type: 'string', value: 'drop table `User`' },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'drop table `User`' },
+ }),
+ name: 'drop table `User`',
+ }),
+ ];
- createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => {
- test('should name spans after the query summary with span streaming', { timeout: 60_000 }, async () => {
await createRunner()
.expect({
span: container => {
- // The `mysql2` driver spans underneath the knex spans come from a different integration and
- // are named after the full statement, so they are filtered out here.
- const knexSpans = container.items.filter(item => item.attributes['sentry.origin']?.value === ORIGIN);
-
- expect(
- knexSpans.map(span => ({
- name: span.name,
- summary: span.attributes['db.query.summary']?.value,
- text: span.attributes['db.query.text']?.value,
- })),
- ).toEqual([
- {
- name: 'create table `User`',
- summary: 'create table `User`',
- text: 'create table `User` (`id` int unsigned not null auto_increment primary key, `createdAt` timestamp(?) not null default CURRENT_TIMESTAMP(?), `email` text not null, `name` text not null)',
- },
- {
- name: 'insert `User`',
- summary: 'insert `User`',
- text: 'insert into `User` (`email`, `name`) values (?, ?)',
- },
- { name: 'select `User`', summary: 'select `User`', text: 'select * from `User`' },
- { name: 'drop table `User`', summary: 'drop table `User`', text: 'drop table `User`' },
- ]);
+ expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction');
+ const knexSpans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === ORIGIN);
+ expect(knexSpans).toEqual(EXPECTED_SPANS);
},
})
.start()
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument-span-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument-span-streaming.mjs
deleted file mode 100644
index 53b9511a21f0..000000000000
--- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument-span-streaming.mjs
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as Sentry from '@sentry/node';
-import { loggingTransport } from '@sentry-internal/node-integration-tests';
-
-Sentry.init({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- release: '1.0',
- tracesSampleRate: 1.0,
- transport: loggingTransport,
- traceLifecycle: 'stream',
-});
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument.mjs
index 170ad6f6a702..46a27dd03b74 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/pg/instrument.mjs
@@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
Sentry.init({
- traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
diff --git a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts
index 5d1da5ad89ff..ae9ea260eab4 100644
--- a/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/knex/pg/test.ts
@@ -1,3 +1,15 @@
+import {
+ DB_NAMESPACE,
+ DB_OPERATION_NAME,
+ DB_QUERY_SUMMARY,
+ DB_QUERY_TEXT,
+ DB_SYSTEM_NAME,
+ SENTRY_OP,
+ SENTRY_ORIGIN,
+ SERVER_ADDRESS,
+ SERVER_PORT,
+} from '@sentry/conventions/attributes';
+import { DB } from '@sentry/conventions/op';
import { describe, expect } from 'vitest';
import { createEsmAndCjsTests, describeWithDockerCompose } from '../../../../utils/runner';
@@ -9,112 +21,89 @@ describe('knex auto instrumentation', () => {
describeWithDockerCompose('with `pg` client', { workingDirectory: [__dirname] }, () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should auto-instrument `knex` package', { timeout: 60_000 }, async () => {
- const EXPECTED_TRANSACTION = {
- transaction: 'Test Transaction',
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.system.name': 'postgresql',
- 'db.namespace': 'tests',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- 'server.address': 'localhost',
- 'server.port': 5445,
- }),
- status: 'ok',
- description:
- 'create table "User" ("id" serial primary key, "createdAt" timestamptz(?) not null default CURRENT_TIMESTAMP(?), "email" text not null, "name" text not null)',
- origin: ORIGIN,
+ const EXPECTED_SPANS = [
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'postgresql' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' },
+ [SERVER_PORT]: { type: 'integer', value: 5445 },
+ [DB_QUERY_TEXT]: {
+ type: 'string',
+ value:
+ 'create table "User" ("id" serial primary key, "createdAt" timestamptz(?) not null default CURRENT_TIMESTAMP(?), "email" text not null, "name" text not null)',
+ },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'create table "User"' },
}),
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.system.name': 'postgresql',
- 'db.namespace': 'tests',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- 'server.address': 'localhost',
- 'server.port': 5445,
- }),
- status: 'ok',
- // In the knex-otel spans, the placeholders (e.g., `$1`) are replaced by a `?`.
- description: 'insert into "User" ("email", "name") values (?, ?)',
- origin: ORIGIN,
+ status: 'ok',
+ name: 'create table "User"',
+ }),
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'postgresql' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [SERVER_ADDRESS]: { type: 'string', value: 'localhost' },
+ [SERVER_PORT]: { type: 'integer', value: 5445 },
+ [DB_QUERY_TEXT]: { type: 'string', value: 'insert into "User" ("email", "name") values (?, ?)' },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'insert "User"' },
}),
+ status: 'ok',
+ // In the knex-otel spans, the placeholders (e.g., `$1`) are replaced by a `?`.
+ name: 'insert "User"',
+ }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.operation.name': 'select',
- 'db.sql.table': 'User',
- 'db.system.name': 'postgresql',
- 'db.namespace': 'tests',
- 'db.query.text': 'select * from "User"',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- }),
- status: 'ok',
- description: 'select * from "User"',
- origin: ORIGIN,
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_OPERATION_NAME]: { type: 'string', value: 'select' },
+ 'db.sql.table': { type: 'string', value: 'User' },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'postgresql' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [DB_QUERY_TEXT]: { type: 'string', value: 'select * from "User"' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'select "User"' },
}),
+ status: 'ok',
+ name: 'select "User"',
+ }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'knex.version': KNEX_VERSION,
- 'db.operation.name': 'select',
- 'db.sql.table': 'DoesNotExist',
- 'db.system.name': 'postgresql',
- 'db.namespace': 'tests',
- 'db.query.text': 'select * from "DoesNotExist"',
- 'sentry.origin': ORIGIN,
- 'sentry.op': 'db',
- }),
- status: 'internal_error',
- description: 'select * from "DoesNotExist"',
- origin: ORIGIN,
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'knex.version': { type: 'string', value: KNEX_VERSION },
+ [DB_OPERATION_NAME]: { type: 'string', value: 'select' },
+ 'db.sql.table': { type: 'string', value: 'DoesNotExist' },
+ [DB_SYSTEM_NAME]: { type: 'string', value: 'postgresql' },
+ [DB_NAMESPACE]: { type: 'string', value: 'tests' },
+ [DB_QUERY_TEXT]: { type: 'string', value: 'select * from "DoesNotExist"' },
+ [SENTRY_ORIGIN]: { type: 'string', value: ORIGIN },
+ [SENTRY_OP]: { type: 'string', value: DB },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'select "DoesNotExist"' },
}),
- ]),
- };
-
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
- });
- });
+ status: 'error',
+ name: 'select "DoesNotExist"',
+ }),
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ [DB_QUERY_TEXT]: { type: 'string', value: 'drop table "User"' },
+ [DB_QUERY_SUMMARY]: { type: 'string', value: 'drop table "User"' },
+ }),
+ name: 'drop table "User"',
+ }),
+ ];
- createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => {
- test('should name spans after the query summary with span streaming', { timeout: 60_000 }, async () => {
await createRunner()
.expect({
span: container => {
- // The `pg` driver spans underneath the knex spans come from a different integration and
- // are named after the full statement, so they are filtered out here.
- const knexSpans = container.items.filter(item => item.attributes['sentry.origin']?.value === ORIGIN);
-
- expect(
- knexSpans.map(span => ({
- name: span.name,
- summary: span.attributes['db.query.summary']?.value,
- text: span.attributes['db.query.text']?.value,
- })),
- ).toEqual([
- {
- name: 'create table "User"',
- summary: 'create table "User"',
- text: 'create table "User" ("id" serial primary key, "createdAt" timestamptz(?) not null default CURRENT_TIMESTAMP(?), "email" text not null, "name" text not null)',
- },
- {
- name: 'insert "User"',
- summary: 'insert "User"',
- text: 'insert into "User" ("email", "name") values (?, ?)',
- },
- { name: 'select "User"', summary: 'select "User"', text: 'select * from "User"' },
- {
- name: 'select "DoesNotExist"',
- summary: 'select "DoesNotExist"',
- text: 'select * from "DoesNotExist"',
- },
- { name: 'drop table "User"', summary: 'drop table "User"', text: 'drop table "User"' },
- ]);
+ expect(container.items.find(span => span.is_segment)?.name).toBe('Test Transaction');
+ const knexSpans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === ORIGIN);
+ expect(knexSpans).toEqual(EXPECTED_SPANS);
},
})
.start()
From b4bd17e1c1f4e2f09fefb12fe07d1f5b2129fab3 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Thu, 8 Oct 2026 13:52:00 +0200
Subject: [PATCH 32/47] chore(nitro): Remove stale bundler-plugin-core rollup
external (#25149)
Niitro depends and imports from `@sentry/bundler-plugins` since 11.0.0.
Nitro's rollup `external` entry for `@sentry/bundler-plugin-core` was
outdated and doesn't match any dependency anymore.
found while working on #25147 for v10 and checking v11
Co-authored-by: Claude Opus 5.5 (1M context)
---
packages/nitro/rollup.npm.config.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/nitro/rollup.npm.config.mjs b/packages/nitro/rollup.npm.config.mjs
index 08f1f53421dd..aebbc9aff288 100644
--- a/packages/nitro/rollup.npm.config.mjs
+++ b/packages/nitro/rollup.npm.config.mjs
@@ -5,7 +5,7 @@ export default [
makeBaseNPMConfig({
entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'],
packageSpecificConfig: {
- external: [/^nitro/, /^srvx/, /^@sentry\/opentelemetry/, '@sentry/bundler-plugin-core'],
+ external: [/^nitro/, /^srvx/, /^@sentry\/opentelemetry/],
},
}),
{ emitCjs: false },
From de43b569afe3dd19733675823e59b6eb3086e872 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 8 Oct 2026 11:52:48 +0000
Subject: [PATCH 33/47] chore(deps-dev): Bump next from 15.5.24 to 15.5.27
(#25171)
Bumps [next](https://github.com/vercel/next.js) from 15.5.24 to 15.5.27.
Release notes
Sourced from next's
releases.
v15.5.27
This release contains security fixes for the following
advisories:
Medium:
v15.5.26
This release contains additional security hardening for
next/og. For more information, check out https://nextjs.org/blog/nextjs-security-update-september-22-2026
v15.5.25
Follow-up release to v15.5.24
re-enabling AVIF Image Optimization when newer versions of
sharp are installed (#97954).
Commits
def7eff
v15.5.27
52c94ab
[lts-maintenance] Scope response cache keys to their source route (#219)
785f4c2
[lts-maintenance] Match Next data paths case-sensitively (#197)
7217d63
[lts-maintenance][webpack] Ensure dynamicParams is
respected in `opengraph-...
d5979ba
[lts-maintenance][ci] Remove PR creation from deploy test workflow (#99093)
812ae7d
[lts-maintenance][test] Adjust deploy assertions for platform drift and
the e...
6936da5
v15.5.26
b49b6ca
[maintenance-lts] Harden next/og SVG serialization
f391939
[15.5.x][test] Disable deploy tests triggering bot challenge (#98132)
013ee1d
v15.5.25
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
packages/nextjs/package.json | 2 +-
yarn.lock | 406 +++++++++++++++++------------------
2 files changed, 204 insertions(+), 204 deletions(-)
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 97488a84ec01..68c9b2baaf57 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -94,7 +94,7 @@
"stacktrace-parser": "^0.1.11"
},
"devDependencies": {
- "next": "15.5.24",
+ "next": "15.5.27",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/yarn.lock b/yarn.lock
index 43f25e6bf314..abf7f58320c1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4927,12 +4927,12 @@
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.2.4"
-"@img/sharp-darwin-arm64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz#bc10b262de2fc80088013f5f998298d1c8909fbf"
- integrity sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==
+"@img/sharp-darwin-arm64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz#b1e0276d4e9bf26a7fde7b2adcbe076c71015fc2"
+ integrity sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==
optionalDependencies:
- "@img/sharp-libvips-darwin-arm64" "1.3.3"
+ "@img/sharp-libvips-darwin-arm64" "1.3.4"
"@img/sharp-darwin-x64@0.33.5":
version "0.33.5"
@@ -4948,19 +4948,19 @@
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.2.4"
-"@img/sharp-darwin-x64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz#76c49ff04fb3f9d846b0d0575841b7ee59beec68"
- integrity sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==
+"@img/sharp-darwin-x64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz#bff811f9610ef322415e87f7551fd5d773122928"
+ integrity sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==
optionalDependencies:
- "@img/sharp-libvips-darwin-x64" "1.3.3"
+ "@img/sharp-libvips-darwin-x64" "1.3.4"
-"@img/sharp-freebsd-wasm32@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz#ba55fd603c5d1d01a1cb14ffb4d970bc6ced80e1"
- integrity sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==
+"@img/sharp-freebsd-wasm32@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz#53b6041958608afa2f5f08323091101407e99086"
+ integrity sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==
dependencies:
- "@img/sharp-wasm32" "0.35.4"
+ "@img/sharp-wasm32" "0.35.5"
"@img/sharp-libvips-darwin-arm64@1.0.4":
version "1.0.4"
@@ -4972,10 +4972,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43"
integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==
-"@img/sharp-libvips-darwin-arm64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz#08a6cf4fb4ee8d45f99404a569dcaa6b29391d97"
- integrity sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==
+"@img/sharp-libvips-darwin-arm64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz#f29cbba302384f9a08bdb04d14c57b5cf71da4a4"
+ integrity sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==
"@img/sharp-libvips-darwin-x64@1.0.4":
version "1.0.4"
@@ -4987,10 +4987,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc"
integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==
-"@img/sharp-libvips-darwin-x64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz#1461e6fb310a869b3c13504589b8dd5c06624da8"
- integrity sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==
+"@img/sharp-libvips-darwin-x64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz#527f52721a89dd75614663aef87162a781739bab"
+ integrity sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==
"@img/sharp-libvips-linux-arm64@1.0.4":
version "1.0.4"
@@ -5002,10 +5002,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318"
integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==
-"@img/sharp-libvips-linux-arm64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz#cb938a3971b9a36329bc99427f1e5b72e266ba2e"
- integrity sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==
+"@img/sharp-libvips-linux-arm64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz#78c11c6b0a4b4045c72dacacdec3b10f3e521349"
+ integrity sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==
"@img/sharp-libvips-linux-arm@1.0.5":
version "1.0.5"
@@ -5017,30 +5017,30 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d"
integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==
-"@img/sharp-libvips-linux-arm@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz#71032941b9fcbf8ebc3c1b62949927b844cbad62"
- integrity sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==
+"@img/sharp-libvips-linux-arm@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz#f5ecd8ebb63690e580438e308ce73015b49516a8"
+ integrity sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==
"@img/sharp-libvips-linux-ppc64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7"
integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==
-"@img/sharp-libvips-linux-ppc64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz#8246d2dec15234fd1cc54cc55dcba1aebc26c414"
- integrity sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==
+"@img/sharp-libvips-linux-ppc64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz#b66a4d25bd042eff66c8076b1e4bd99c6d19246e"
+ integrity sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==
"@img/sharp-libvips-linux-riscv64@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de"
integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==
-"@img/sharp-libvips-linux-riscv64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz#59ff663c2aa6b418857357cd77ca4ccaece95876"
- integrity sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==
+"@img/sharp-libvips-linux-riscv64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz#9dee8b3fd801c0ff5d6083d6558d3c89b8dedc1b"
+ integrity sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==
"@img/sharp-libvips-linux-s390x@1.0.4":
version "1.0.4"
@@ -5052,10 +5052,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec"
integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==
-"@img/sharp-libvips-linux-s390x@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz#bc17451f80d9f31663b3e906149f159a0ecb1ca8"
- integrity sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==
+"@img/sharp-libvips-linux-s390x@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz#55a80f118f9ffd7653d20cefde41fca6ba0241e2"
+ integrity sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==
"@img/sharp-libvips-linux-x64@1.0.4":
version "1.0.4"
@@ -5067,10 +5067,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce"
integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
-"@img/sharp-libvips-linux-x64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz#c7d1f83bc1cfe222cd9dc927f3c5137aa9791e76"
- integrity sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==
+"@img/sharp-libvips-linux-x64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz#a0de3840ae386476eb4045d6d2b056a8062c548b"
+ integrity sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==
"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
version "1.0.4"
@@ -5082,10 +5082,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06"
integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==
-"@img/sharp-libvips-linuxmusl-arm64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz#303dfe2a8e6d1fd279ead559f4d2ccff2545e2a2"
- integrity sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==
+"@img/sharp-libvips-linuxmusl-arm64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz#420f2418eaec33cef4346e43d96a7864bb3fcbb4"
+ integrity sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==
"@img/sharp-libvips-linuxmusl-x64@1.0.4":
version "1.0.4"
@@ -5097,10 +5097,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75"
integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
-"@img/sharp-libvips-linuxmusl-x64@1.3.3":
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz#3750f70ad46ad87e4a08ee8d372ab9a7e17e2b6c"
- integrity sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==
+"@img/sharp-libvips-linuxmusl-x64@1.3.4":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz#eb5696f7fa521f5f7f3d962fd9ade41deeba6a51"
+ integrity sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==
"@img/sharp-linux-arm64@0.33.5":
version "0.33.5"
@@ -5116,12 +5116,12 @@
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.2.4"
-"@img/sharp-linux-arm64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz#c2f53bf913fa1b46762b192c527a2b3647a211a8"
- integrity sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==
+"@img/sharp-linux-arm64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz#ffaea1b88310b5bc40de48859a7f8f01d07e5e99"
+ integrity sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==
optionalDependencies:
- "@img/sharp-libvips-linux-arm64" "1.3.3"
+ "@img/sharp-libvips-linux-arm64" "1.3.4"
"@img/sharp-linux-arm@0.33.5":
version "0.33.5"
@@ -5137,12 +5137,12 @@
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.2.4"
-"@img/sharp-linux-arm@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz#de8da0da2ee552267cae771597332a863378e43b"
- integrity sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==
+"@img/sharp-linux-arm@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz#950645b8695c200bfdb3c6eccf17c32c26968c23"
+ integrity sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==
optionalDependencies:
- "@img/sharp-libvips-linux-arm" "1.3.3"
+ "@img/sharp-libvips-linux-arm" "1.3.4"
"@img/sharp-linux-ppc64@0.34.5":
version "0.34.5"
@@ -5151,12 +5151,12 @@
optionalDependencies:
"@img/sharp-libvips-linux-ppc64" "1.2.4"
-"@img/sharp-linux-ppc64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz#c40d7c5f8ce216c35224c58edcef8e6445935256"
- integrity sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==
+"@img/sharp-linux-ppc64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz#af60c7db090411f7af2df2bc1aa764b93b6b8add"
+ integrity sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==
optionalDependencies:
- "@img/sharp-libvips-linux-ppc64" "1.3.3"
+ "@img/sharp-libvips-linux-ppc64" "1.3.4"
"@img/sharp-linux-riscv64@0.34.5":
version "0.34.5"
@@ -5165,12 +5165,12 @@
optionalDependencies:
"@img/sharp-libvips-linux-riscv64" "1.2.4"
-"@img/sharp-linux-riscv64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz#032ca206e713b1179efe0e18d525188c67277730"
- integrity sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==
+"@img/sharp-linux-riscv64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz#afcd42ac0592330fbf4fff5904574e0e049ba0a4"
+ integrity sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==
optionalDependencies:
- "@img/sharp-libvips-linux-riscv64" "1.3.3"
+ "@img/sharp-libvips-linux-riscv64" "1.3.4"
"@img/sharp-linux-s390x@0.33.5":
version "0.33.5"
@@ -5186,12 +5186,12 @@
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.2.4"
-"@img/sharp-linux-s390x@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz#19dac7501a15c8748bc7a11219fca391e424b6d7"
- integrity sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==
+"@img/sharp-linux-s390x@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz#092e584a8f36f2f90ba98652d6f45432edea87cb"
+ integrity sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==
optionalDependencies:
- "@img/sharp-libvips-linux-s390x" "1.3.3"
+ "@img/sharp-libvips-linux-s390x" "1.3.4"
"@img/sharp-linux-x64@0.33.5":
version "0.33.5"
@@ -5207,12 +5207,12 @@
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.2.4"
-"@img/sharp-linux-x64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz#7d1d414d4a538e0704ba6b76ce6547e60f6211ce"
- integrity sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==
+"@img/sharp-linux-x64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz#f624f6b98c6579084b9fb363d7cb83e2315de79c"
+ integrity sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==
optionalDependencies:
- "@img/sharp-libvips-linux-x64" "1.3.3"
+ "@img/sharp-libvips-linux-x64" "1.3.4"
"@img/sharp-linuxmusl-arm64@0.33.5":
version "0.33.5"
@@ -5228,12 +5228,12 @@
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
-"@img/sharp-linuxmusl-arm64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz#9534c93807822f4282d079a322facc6db53d50f6"
- integrity sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==
+"@img/sharp-linuxmusl-arm64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz#92d0904211086d10bd3c929de715dd8a81fa7268"
+ integrity sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==
optionalDependencies:
- "@img/sharp-libvips-linuxmusl-arm64" "1.3.3"
+ "@img/sharp-libvips-linuxmusl-arm64" "1.3.4"
"@img/sharp-linuxmusl-x64@0.33.5":
version "0.33.5"
@@ -5249,12 +5249,12 @@
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.2.4"
-"@img/sharp-linuxmusl-x64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz#f488e7a0291991bff643cef82b5afd3cfd70fcf2"
- integrity sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==
+"@img/sharp-linuxmusl-x64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz#1d146eab6dfcdc6429f29b032b8bc3f0180b6d80"
+ integrity sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==
optionalDependencies:
- "@img/sharp-libvips-linuxmusl-x64" "1.3.3"
+ "@img/sharp-libvips-linuxmusl-x64" "1.3.4"
"@img/sharp-wasm32@0.33.5":
version "0.33.5"
@@ -5270,29 +5270,29 @@
dependencies:
"@emnapi/runtime" "^1.7.0"
-"@img/sharp-wasm32@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz#81f71f3fc0d8447dc3af6cb378e24e758ff54225"
- integrity sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==
+"@img/sharp-wasm32@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz#5369559c3a4c32e08ce92b6ffba2434175a45a62"
+ integrity sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==
dependencies:
"@emnapi/runtime" "^1.11.3"
-"@img/sharp-webcontainers-wasm32@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz#77d2e8892d3a3825f0dc95ab02ceb613cb213286"
- integrity sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==
+"@img/sharp-webcontainers-wasm32@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz#3674eddf41eb85d0e92896d7da01ca00310256b9"
+ integrity sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==
dependencies:
- "@img/sharp-wasm32" "0.35.4"
+ "@img/sharp-wasm32" "0.35.5"
"@img/sharp-win32-arm64@0.34.5":
version "0.34.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a"
integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==
-"@img/sharp-win32-arm64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz#4101b44e3fb97d6f1a73c2a9c6142d491d1aa831"
- integrity sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==
+"@img/sharp-win32-arm64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz#e3c94326eb0a46ade843b84cf50a3917b79d323f"
+ integrity sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==
"@img/sharp-win32-ia32@0.33.5":
version "0.33.5"
@@ -5304,10 +5304,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de"
integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==
-"@img/sharp-win32-ia32@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz#8d4755bd26d5ff34fb89c7907bd89c080a5dc9f5"
- integrity sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==
+"@img/sharp-win32-ia32@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz#cc5dac453dc3500aac2e63f182b952af0d437bc1"
+ integrity sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==
"@img/sharp-win32-x64@0.33.5":
version "0.33.5"
@@ -5319,10 +5319,10 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
-"@img/sharp-win32-x64@0.35.4":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz#0b67bd0b1f01123a982afcc6ba0dc00b877b8a0a"
- integrity sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==
+"@img/sharp-win32-x64@0.35.5":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz#1785356b8cb396e58430e03865281280acb818b1"
+ integrity sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==
"@ioredis/commands@1.10.0":
version "1.10.0"
@@ -5719,50 +5719,50 @@
path-to-regexp "8.4.2"
tslib "2.8.1"
-"@next/env@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.24.tgz#baee5641d0d93acac87618e1b8004acce594bbad"
- integrity sha512-mBDF7T0XKZjs9SpUAl0buizVO+O02ULjOvWX8o/AZo/5AGw/UAS1Zzcylmd4pqbftzmKQi+L/nB4jgBYKEAl5Q==
-
-"@next/swc-darwin-arm64@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.24.tgz#7a9134e91872bcdb80f070f409903eb81780d04b"
- integrity sha512-AGdNLvxZNY6eR2iSnV+6wUa8CiHTMr4F7g3uHH7fT4ICIJBE00R9u4tzN/Vuwsw0cOi8MTD2HJcTCb6siMH88Q==
-
-"@next/swc-darwin-x64@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.24.tgz#67a91d19b1618b5d1237d874cb52042173ab3269"
- integrity sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==
-
-"@next/swc-linux-arm64-gnu@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.24.tgz#b235d3e13c65aea3ed9dc1dde5b557b306969223"
- integrity sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==
-
-"@next/swc-linux-arm64-musl@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.24.tgz#c3a3dd495602cf2b79e72d008226b4e438cfe321"
- integrity sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==
-
-"@next/swc-linux-x64-gnu@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.24.tgz#b1c74074f90d2cb2a2d1fe6dda5a73b8aa9f2151"
- integrity sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==
-
-"@next/swc-linux-x64-musl@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.24.tgz#528bba9b6818c92ae6da6b0f3db1aef690d7ed86"
- integrity sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==
-
-"@next/swc-win32-arm64-msvc@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.24.tgz#1257886635fe6a3dc873cd2b0c6134b601a60cf8"
- integrity sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==
-
-"@next/swc-win32-x64-msvc@15.5.24":
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.24.tgz#4b86cda0ebfba20b2dac1f20abc56630be5c05c9"
- integrity sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==
+"@next/env@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.27.tgz#4c4ec6e4a38c5d654bae847426959e84bfca7229"
+ integrity sha512-WC5hvVqiuFOcRyz6gKPeJkiVvSMf3scNeAV49CdWtVqxmUlzh8UKR6YHt9yiNkh8LngtzqSlVz07/Ma3ZAzchA==
+
+"@next/swc-darwin-arm64@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.27.tgz#b119e1e3e6c3b8ceca7a8613021442f94ac9c4cb"
+ integrity sha512-fBA9QurmK4+uj4wNhmRTuO5zMVRaBHCvp6H93zTG/5i6Yi6TewbvbesXB8YQNj8T99Cwrow87hHiFV6wL/hyMA==
+
+"@next/swc-darwin-x64@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.27.tgz#c40d0c9866a3395e9abee09ad352cdec28724e6c"
+ integrity sha512-FGZwJwry8qZNHxD/nfwrCwCHHrqx6epqItkkxbYcBQEQ2uw5S43bY83fQB5zbAsZE7TELMjoYkbhsQ4oKM7wwg==
+
+"@next/swc-linux-arm64-gnu@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.27.tgz#44ace267964a522f7df666bbde2a9e241374b722"
+ integrity sha512-GQulAaJYNF1RYuatlsw9/uzbKz4if2V7Ic9nVO+P0ljYPm4yBRTFIl6g5YkKTw40lMLbpy9edLoH44g3jSHMYA==
+
+"@next/swc-linux-arm64-musl@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.27.tgz#a2867ac0fe08f14d0f35780d0b05ade53c36cb43"
+ integrity sha512-URFFsUmu8Pi0/1qdnUI7tx7keZw1Mt5zHfrIX1RfDG9Nih8DYxK54WZsDJUX2ZTlii1jgotL7qcbwKnSAES6xQ==
+
+"@next/swc-linux-x64-gnu@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.27.tgz#a9a98bc6e9093f1983325202718981cc8b98b089"
+ integrity sha512-aJp74z7uvU5jQhyH7lyk+4qSRcTdKbKT9NQE3jiO9kWYkoHZjKi0zzHDbCoZ+v2sJDEiJ/z58R0UlWMGWNF3HA==
+
+"@next/swc-linux-x64-musl@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.27.tgz#8f5e14ed78c4e12845383ddabad3c1473ce001d3"
+ integrity sha512-ILl2bQ8MbpvyMpY8AXljb3BXs0MJSC8HXZ6QV5AWpVS7ZELqNPtrHvhUk+SNarvjvICXX8bCS287TCi+gUFVYg==
+
+"@next/swc-win32-arm64-msvc@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.27.tgz#1f0542804a2b03e0748c59543cdcc911de3cb08d"
+ integrity sha512-1MB9Rh5y8TOPN2x7naNTJNkCZQav9c70AI7EBYXxsEewwOffC4DjjwBwKf20NBvcA+u+5jDv7nVmHxunlP2d7Q==
+
+"@next/swc-win32-x64-msvc@15.5.27":
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.27.tgz#d3f0e0fe2f0edd0611f1d68f78bb1e366a6546be"
+ integrity sha512-XtYgpGOswgnyBWXzOEG6q9RhvnSw0CEZ0MbWqCsl5PISgt9EzvJvNW5mU5n1PsphX/lnamzxjgQXH2GUJL8nqg==
"@ngtools/webpack@14.2.13":
version "14.2.13"
@@ -20461,26 +20461,26 @@ new-find-package-json@^2.0.0:
dependencies:
debug "^4.3.4"
-next@15.5.24:
- version "15.5.24"
- resolved "https://registry.yarnpkg.com/next/-/next-15.5.24.tgz#27852f3cfcaeb2631fbd4795cd9d529081e8af70"
- integrity sha512-Y+xn8EQCoC3ZbsFPyzE+tE8XOdrWeUdUF7NeXbmg9DsgAxl5UYxlsrvgVESHTyTGigoTa1bCUrxn70F5bqt0Gw==
+next@15.5.27:
+ version "15.5.27"
+ resolved "https://registry.yarnpkg.com/next/-/next-15.5.27.tgz#bd149e4dcc483bfff31fa390842cfb8b7ff9a522"
+ integrity sha512-F82CrlPZ8GRxBH9RIIoR3qU1t5RT3dv1K8moe8SjlDEquiFipvEI13R1Om90TaUOEwShkWCMlg8+gvTNzfMhvg==
dependencies:
- "@next/env" "15.5.24"
+ "@next/env" "15.5.27"
"@swc/helpers" "0.5.15"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
- "@next/swc-darwin-arm64" "15.5.24"
- "@next/swc-darwin-x64" "15.5.24"
- "@next/swc-linux-arm64-gnu" "15.5.24"
- "@next/swc-linux-arm64-musl" "15.5.24"
- "@next/swc-linux-x64-gnu" "15.5.24"
- "@next/swc-linux-x64-musl" "15.5.24"
- "@next/swc-win32-arm64-msvc" "15.5.24"
- "@next/swc-win32-x64-msvc" "15.5.24"
- sharp "^0.34.3 || ^0.35.3"
+ "@next/swc-darwin-arm64" "15.5.27"
+ "@next/swc-darwin-x64" "15.5.27"
+ "@next/swc-linux-arm64-gnu" "15.5.27"
+ "@next/swc-linux-arm64-musl" "15.5.27"
+ "@next/swc-linux-x64-gnu" "15.5.27"
+ "@next/swc-linux-x64-musl" "15.5.27"
+ "@next/swc-win32-arm64-msvc" "15.5.27"
+ "@next/swc-win32-x64-msvc" "15.5.27"
+ sharp "^0.34.3 || ^0.35.4"
nf3@^0.3.11, nf3@^0.3.16:
version "0.3.16"
@@ -24932,40 +24932,40 @@ sharp@^0.33.3:
"@img/sharp-win32-ia32" "0.33.5"
"@img/sharp-win32-x64" "0.33.5"
-"sharp@^0.34.3 || ^0.35.3":
- version "0.35.4"
- resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.35.4.tgz#361df3b2959daeb380541289960456c6be0f92de"
- integrity sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==
+"sharp@^0.34.3 || ^0.35.4":
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.35.5.tgz#43fb539872ec5f53cc7162eb850dbf6514692fbf"
+ integrity sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==
dependencies:
"@img/colour" "^1.1.0"
detect-libc "^2.1.2"
semver "^7.8.5"
optionalDependencies:
- "@img/sharp-darwin-arm64" "0.35.4"
- "@img/sharp-darwin-x64" "0.35.4"
- "@img/sharp-freebsd-wasm32" "0.35.4"
- "@img/sharp-libvips-darwin-arm64" "1.3.3"
- "@img/sharp-libvips-darwin-x64" "1.3.3"
- "@img/sharp-libvips-linux-arm" "1.3.3"
- "@img/sharp-libvips-linux-arm64" "1.3.3"
- "@img/sharp-libvips-linux-ppc64" "1.3.3"
- "@img/sharp-libvips-linux-riscv64" "1.3.3"
- "@img/sharp-libvips-linux-s390x" "1.3.3"
- "@img/sharp-libvips-linux-x64" "1.3.3"
- "@img/sharp-libvips-linuxmusl-arm64" "1.3.3"
- "@img/sharp-libvips-linuxmusl-x64" "1.3.3"
- "@img/sharp-linux-arm" "0.35.4"
- "@img/sharp-linux-arm64" "0.35.4"
- "@img/sharp-linux-ppc64" "0.35.4"
- "@img/sharp-linux-riscv64" "0.35.4"
- "@img/sharp-linux-s390x" "0.35.4"
- "@img/sharp-linux-x64" "0.35.4"
- "@img/sharp-linuxmusl-arm64" "0.35.4"
- "@img/sharp-linuxmusl-x64" "0.35.4"
- "@img/sharp-webcontainers-wasm32" "0.35.4"
- "@img/sharp-win32-arm64" "0.35.4"
- "@img/sharp-win32-ia32" "0.35.4"
- "@img/sharp-win32-x64" "0.35.4"
+ "@img/sharp-darwin-arm64" "0.35.5"
+ "@img/sharp-darwin-x64" "0.35.5"
+ "@img/sharp-freebsd-wasm32" "0.35.5"
+ "@img/sharp-libvips-darwin-arm64" "1.3.4"
+ "@img/sharp-libvips-darwin-x64" "1.3.4"
+ "@img/sharp-libvips-linux-arm" "1.3.4"
+ "@img/sharp-libvips-linux-arm64" "1.3.4"
+ "@img/sharp-libvips-linux-ppc64" "1.3.4"
+ "@img/sharp-libvips-linux-riscv64" "1.3.4"
+ "@img/sharp-libvips-linux-s390x" "1.3.4"
+ "@img/sharp-libvips-linux-x64" "1.3.4"
+ "@img/sharp-libvips-linuxmusl-arm64" "1.3.4"
+ "@img/sharp-libvips-linuxmusl-x64" "1.3.4"
+ "@img/sharp-linux-arm" "0.35.5"
+ "@img/sharp-linux-arm64" "0.35.5"
+ "@img/sharp-linux-ppc64" "0.35.5"
+ "@img/sharp-linux-riscv64" "0.35.5"
+ "@img/sharp-linux-s390x" "0.35.5"
+ "@img/sharp-linux-x64" "0.35.5"
+ "@img/sharp-linuxmusl-arm64" "0.35.5"
+ "@img/sharp-linuxmusl-x64" "0.35.5"
+ "@img/sharp-webcontainers-wasm32" "0.35.5"
+ "@img/sharp-win32-arm64" "0.35.5"
+ "@img/sharp-win32-ia32" "0.35.5"
+ "@img/sharp-win32-x64" "0.35.5"
sharp@^0.34.5:
version "0.34.5"
From 540c79858013c1230500b641a81f31eae05d885c Mon Sep 17 00:00:00 2001
From: Andrei <168741329+andreiborza@users.noreply.github.com>
Date: Thu, 8 Oct 2026 14:09:04 +0200
Subject: [PATCH 34/47] feat(server-utils): Record LangChain TypeSafeClassifier
runs as gen_ai.evaluate spans (#25120)
## What
`TypeSafeClassifier` runs from `@langchain/typesafe` now show up as
`gen_ai.evaluate` spans with model, provider, token usage, and the
state, questions and answers when gen_ai recording is on.
- Standalone `classifier.invoke()` calls are traced automatically.
- Calls from inside agents (for example the TypeSafe model router and
auto-mode middleware) are traced too.
## Why
The classifier calls Jev with `fetch` directly, so the TypeSafe
integration did not see it, and LangChain reported it as a generic
`invoke_agent` span without any evaluation data.
Closes: #24859
---------
Co-authored-by: Claude Opus 5.5
Co-authored-by: Charly Gomez
---
...ypesafe-classifier-inherited-callbacks.mjs | 61 +++++++++++++
.../v1/scenario-typesafe-classifier.mjs | 43 ++++++++++
.../suites/tracing/langchain/v1/test.ts | 84 ++++++++++++++++++
.../server-utils/src/ai/langchain/index.ts | 26 ++++--
.../src/ai/langchain/typesafe-classifier.ts | 86 +++++++++++++++++++
.../server-utils/src/ai/langchain/utils.ts | 16 ++++
.../server-utils/src/ai/typesafe/index.ts | 10 ++-
.../src/integrations/langchain.ts | 30 +++++--
.../config/channel-integration-definitions.ts | 1 +
.../src/orchestrion/config/langchain.ts | 15 +++-
.../tracing/langchain-invoke-agent.test.ts | 36 +++++++-
11 files changed, 393 insertions(+), 15 deletions(-)
create mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier-inherited-callbacks.mjs
create mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier.mjs
create mode 100644 packages/server-utils/src/ai/langchain/typesafe-classifier.ts
diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier-inherited-callbacks.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier-inherited-callbacks.mjs
new file mode 100644
index 000000000000..642945063c88
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier-inherited-callbacks.mjs
@@ -0,0 +1,61 @@
+import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
+import { RunnableLambda } from '@langchain/core/runnables';
+import { TypeSafeClassifier } from '@langchain/typesafe';
+// Sets up the AsyncLocalStorage that passes a parent run's config to its children, as it is inside `createAgent`.
+import '@langchain/langgraph';
+import * as Sentry from '@sentry/node';
+import express from 'express';
+
+function startMockTypeSafeServer() {
+ const app = express();
+ app.use(express.json());
+
+ app.post('/v1/systemone', (req, res) => {
+ res.json({
+ model: 'jev-1.13',
+ answers: { urgent: { type: 'noul', noul: 0.9 } },
+ usage: { input_tokens: 30, output_tokens: 2 },
+ });
+ });
+
+ return new Promise(resolve => {
+ const server = app.listen(0, () => {
+ resolve(server);
+ });
+ });
+}
+
+// Stands in for a tracer such as LangSmith, which the user passes to the parent run.
+class RecordingHandler extends BaseCallbackHandler {
+ name = 'RecordingHandler';
+ runs = [];
+
+ handleChainStart(chain, _inputs, _runId, parentRunId) {
+ this.runs.push(`${chain.id.at(-1)}:${parentRunId ? 'child' : 'root'}`);
+ }
+}
+
+async function run() {
+ const server = await startMockTypeSafeServer();
+ const baseUrl = `http://localhost:${server.address().port}`;
+
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async span => {
+ const classifier = new TypeSafeClassifier({
+ apiKey: 'mock-api-key',
+ baseUrl,
+ questions: { urgent: { type: 'noul', instructions: 'Is this urgent?' } },
+ });
+
+ // Like the `@langchain/typesafe` middlewares, call the classifier without a config.
+ const parent = RunnableLambda.from(input => classifier.invoke(input)).withConfig({ runName: 'parent' });
+ const recorder = new RecordingHandler();
+ await parent.invoke('My payouts have been failing.', { callbacks: [recorder] });
+
+ span.setAttribute('test.recorded_runs', recorder.runs.join(','));
+ });
+
+ await Sentry.flush(2000);
+ server.close();
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier.mjs
new file mode 100644
index 000000000000..eaaf7a9c7e52
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-typesafe-classifier.mjs
@@ -0,0 +1,43 @@
+import { HumanMessage } from '@langchain/core/messages';
+import { TypeSafeClassifier } from '@langchain/typesafe';
+import * as Sentry from '@sentry/node';
+import express from 'express';
+
+function startMockTypeSafeServer() {
+ const app = express();
+ app.use(express.json());
+
+ app.post('/v1/systemone', (req, res) => {
+ res.json({
+ model: 'jev-1.13',
+ answers: { urgent: { type: 'noul', noul: 0.9 } },
+ usage: { input_tokens: 30, output_tokens: 2 },
+ });
+ });
+
+ return new Promise(resolve => {
+ const server = app.listen(0, () => {
+ resolve(server);
+ });
+ });
+}
+
+async function run() {
+ const server = await startMockTypeSafeServer();
+ const baseUrl = `http://localhost:${server.address().port}`;
+
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
+ const classifier = new TypeSafeClassifier({
+ apiKey: 'mock-api-key',
+ baseUrl,
+ questions: { urgent: { type: 'noul', instructions: 'Is this urgent?' } },
+ });
+
+ await classifier.invoke('My payouts have been failing.');
+ await classifier.invoke(new HumanMessage('My card was charged twice.'));
+ });
+
+ server.close();
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts
index 41158cb85db1..ae4f7a1554ef 100644
--- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts
@@ -3,6 +3,7 @@ import {
GEN_AI_CONVERSATION_ID,
GEN_AI_INPUT_MESSAGES,
GEN_AI_OPERATION_NAME,
+ GEN_AI_OUTPUT_MESSAGES,
GEN_AI_PROVIDER_NAME,
GEN_AI_REQUEST_MAX_TOKENS,
GEN_AI_REQUEST_MODEL,
@@ -18,6 +19,7 @@ import {
SENTRY_OP,
SENTRY_ORIGIN,
} from '@sentry/conventions/attributes';
+import { GEN_AI_EVALUATE } from '@sentry/conventions/op';
import { GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
import { createEsmTests } from '../../../../utils/runner/createEsmAndCjsTests';
@@ -233,6 +235,88 @@ describe('LangChain integration (v1)', () => {
},
);
+ createEsmAndCjsTests(
+ __dirname,
+ 'scenario-typesafe-classifier.mjs',
+ 'instrument-with-pii.mjs',
+ (createRunner, test) => {
+ test('records a TypeSafeClassifier run as a gen_ai.evaluate span', async () => {
+ const runner = createRunner().ignore('event');
+ const spansPromise = runner.collectStreamedSpansUntilSegment('main');
+
+ await runner.start().completed();
+
+ const spans = (await spansPromise).filter(
+ span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.langchain',
+ );
+ expect(spans.map(span => span.name)).toEqual(['evaluate jev-latest', 'evaluate jev-latest']);
+
+ const [evaluateSpan, messageEvaluateSpan] = spans.sort((a, b) => a.start_timestamp - b.start_timestamp);
+ expect(evaluateSpan.attributes[SENTRY_OP].value).toBe(GEN_AI_EVALUATE);
+ expect(evaluateSpan.attributes[SENTRY_ORIGIN].value).toBe('auto.ai.langchain');
+ expect(evaluateSpan.attributes[GEN_AI_OPERATION_NAME].value).toBe('evaluate');
+ expect(evaluateSpan.attributes[GEN_AI_PROVIDER_NAME].value).toBe('typesafe');
+ expect(evaluateSpan.attributes[GEN_AI_REQUEST_MODEL].value).toBe('jev-latest');
+ expect(evaluateSpan.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('jev-1.13');
+ expect(evaluateSpan.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(30);
+ expect(evaluateSpan.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(2);
+ expect(evaluateSpan.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(32);
+ expect(JSON.parse(evaluateSpan.attributes[GEN_AI_INPUT_MESSAGES].value)).toEqual([
+ {
+ type: 'evaluation',
+ state: 'My payouts have been failing.',
+ questions: { urgent: { type: 'noul', instructions: 'Is this urgent?' } },
+ },
+ ]);
+ // A message is recorded as the transcript line the classifier sends, not as LangChain's serialized form.
+ expect(JSON.parse(messageEvaluateSpan!.attributes[GEN_AI_INPUT_MESSAGES].value)).toEqual([
+ {
+ type: 'evaluation',
+ state: 'user: My card was charged twice.',
+ questions: { urgent: { type: 'noul', instructions: 'Is this urgent?' } },
+ },
+ ]);
+ expect(JSON.parse(evaluateSpan.attributes[GEN_AI_OUTPUT_MESSAGES].value)).toEqual([
+ { type: 'evaluation', answers: { urgent: { type: 'noul', noul: 0.9 } } },
+ ]);
+ });
+ },
+ {
+ additionalDependencies: {
+ langchain: '^1.0.0',
+ '@langchain/core': '^1.0.0',
+ '@langchain/typesafe': '^0.0.2',
+ },
+ },
+ );
+
+ createEsmAndCjsTests(
+ __dirname,
+ 'scenario-typesafe-classifier-inherited-callbacks.mjs',
+ 'instrument-with-pii.mjs',
+ (createRunner, test) => {
+ test('keeps the parent run callbacks for a TypeSafeClassifier called without config', async () => {
+ const runner = createRunner().ignore('event');
+ const spansPromise = runner.collectStreamedSpansUntilSegment('main');
+
+ await runner.start().completed();
+
+ const spans = await spansPromise;
+ const segment = spans.find(span => span.is_segment && span.name === 'main');
+ expect(segment!.attributes['test.recorded_runs'].value).toBe('RunnableLambda:root,TypeSafeClassifier:child');
+ expect(spans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_EVALUATE)).toHaveLength(1);
+ });
+ },
+ {
+ additionalDependencies: {
+ langchain: '^1.0.0',
+ '@langchain/core': '^1.0.0',
+ '@langchain/typesafe': '^0.0.2',
+ '@langchain/langgraph': '^1.0.0',
+ },
+ },
+ );
+
createEsmTests(
__dirname,
'scenario-openai-before-langchain.mjs',
diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts
index 9088e24cc523..f7b5408d1d59 100644
--- a/packages/server-utils/src/ai/langchain/index.ts
+++ b/packages/server-utils/src/ai/langchain/index.ts
@@ -15,6 +15,11 @@ import {
import { GEN_AI_CHAT, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op';
import { resolveAIRecordingOptions } from '../core/utils';
import { LANGCHAIN_ORIGIN } from './constants';
+import {
+ addTypeSafeClassifierResponseAttributes,
+ isTypeSafeClassifier,
+ startTypeSafeClassifierSpan,
+} from './typesafe-classifier';
import type {
LangChainCallbackHandler,
LangChainLLMResult,
@@ -43,6 +48,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
// Internal state - single instance tracks all spans
const spanMap = new Map();
+ const evaluateRunIds = new Set();
/**
* Exit a span and clean up
@@ -51,8 +57,9 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
const span = spanMap.get(runId);
if (span?.isRecording()) {
span.end();
- spanMap.delete(runId);
}
+ spanMap.delete(runId);
+ evaluateRunIds.delete(runId);
};
/**
@@ -215,6 +222,13 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
_runType?: string,
runName?: string,
) {
+ // A Jev call is a real model call, so it is recorded inside an agent too.
+ if (isTypeSafeClassifier(chain)) {
+ spanMap.set(runId, startTypeSafeClassifierSpan(chain, inputs, metadata, recordInputs));
+ evaluateRunIds.add(runId);
+ return;
+ }
+
// Skip chain spans when inside an agent context (createReactAgent).
// The agent already creates an invoke_agent span; internal chain steps
// (ChannelWrite, Branch, prompt, etc.) are noise.
@@ -264,14 +278,16 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
handleChainEnd(outputs: unknown, runId: string) {
const span = spanMap.get(runId);
if (span?.isRecording()) {
- // Add outputs if recordOutputs is enabled
- if (recordOutputs) {
+ if (evaluateRunIds.has(runId)) {
+ addTypeSafeClassifierResponseAttributes(span, outputs, recordOutputs);
+ } else if (recordOutputs) {
span.setAttributes({
'langchain.chain.outputs': JSON.stringify(outputs),
});
}
- exitSpan(runId);
}
+ // Also for a sampled-out span, which would otherwise stay tracked.
+ exitSpan(runId);
},
// Chain Error Handler
@@ -281,8 +297,8 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
const span = spanMap.get(runId);
if (span?.isRecording()) {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
- exitSpan(runId);
}
+ exitSpan(runId);
},
// Tool Start Handler
diff --git a/packages/server-utils/src/ai/langchain/typesafe-classifier.ts b/packages/server-utils/src/ai/langchain/typesafe-classifier.ts
new file mode 100644
index 000000000000..e254685a4561
--- /dev/null
+++ b/packages/server-utils/src/ai/langchain/typesafe-classifier.ts
@@ -0,0 +1,86 @@
+import { SENTRY_ORIGIN } from '@sentry/conventions/attributes';
+import type { Span } from '@sentry/core';
+import { isObjectLike } from '@sentry/core';
+import { addResponseAttributes, startEvaluateSpan } from '../typesafe';
+import { LANGCHAIN_ORIGIN } from './constants';
+import { getAgentNameFromMetadata, getConversationIdFromMetadata } from './utils';
+
+// `TypeSafeClassifier` from `@langchain/typesafe` calls Jev with `fetch`, not through `@typesafe-ai/sdk`,
+// so the TypeSafe integration does not see it. Its serialized id is `[...lc_namespace, lc_name()]`; only
+// the class name is matched, as the namespace is the part that changes between versions.
+const TYPESAFE_CLASSIFIER_NAME = 'TypeSafeClassifier';
+
+/** The package's default, used when the classifier is constructed without a `model`. */
+const DEFAULT_TYPESAFE_CLASSIFIER_MODEL = 'jev-latest';
+
+// The `state` the classifier sends to Jev (messages rendered as transcript lines), keyed by the input
+// passed to `invoke()`. LangChain hands that same object to `handleChainStart`, so the span records what
+// Jev received rather than LangChain's serialized messages.
+const wireStates = new WeakMap |