diff --git a/.size-limit.js b/.size-limit.js
index 10efb849a582..c3105a772987 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -206,7 +206,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
- limit: '42 KB',
+ limit: '43 KB',
},
// SvelteKit SDK (ESM)
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62e9251f671f..1823b8c57462 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,15 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 9.27.0
+
+- feat(node): Expand how vercel ai input/outputs can be set ([#16455](https://github.com/getsentry/sentry-javascript/pull/16455))
+- feat(node): Switch to new semantic conventions for Vercel AI ([#16476](https://github.com/getsentry/sentry-javascript/pull/16476))
+- feat(react-router): Add component annotation plugin ([#16472](https://github.com/getsentry/sentry-javascript/pull/16472))
+- feat(react-router): Export wrappers for server loaders and actions ([#16481](https://github.com/getsentry/sentry-javascript/pull/16481))
+- fix(browser): Ignore unrealistically long INP values ([#16484](https://github.com/getsentry/sentry-javascript/pull/16484))
+- fix(react-router): Conditionally add `ReactRouterServer` integration ([#16470](https://github.com/getsentry/sentry-javascript/pull/16470))
+
## 9.26.0
- feat(react-router): Re-export functions from `@sentry/react` ([#16465](https://github.com/getsentry/sentry-javascript/pull/16465))
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
index 8c3e51b14024..f355654bf6a2 100644
--- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
@@ -50,6 +50,7 @@ const DEPENDENTS: Dependent[] = [
ignoreExports: [
// not supported in bun:
'NodeClient',
+ 'NODE_VERSION',
'childProcessIntegration',
],
},
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.gitignore
new file mode 100644
index 000000000000..ebb991370034
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.gitignore
@@ -0,0 +1,32 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
+
+# react router
+.react-router
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/app.css b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/app.css
new file mode 100644
index 000000000000..b31c3a9d0ddf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/app.css
@@ -0,0 +1,6 @@
+html,
+body {
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/entry.client.tsx
new file mode 100644
index 000000000000..925c1e6ab143
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/entry.client.tsx
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/react-router';
+import { StrictMode, startTransition } from 'react';
+import { hydrateRoot } from 'react-dom/client';
+import { HydratedRouter } from 'react-router/dom';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ // todo: get this from env
+ dsn: 'https://username@domain/123',
+ tunnel: `http://localhost:3031/`, // proxy server
+ integrations: [Sentry.reactRouterTracingIntegration()],
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/^\//],
+});
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+ ,
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/entry.server.tsx
new file mode 100644
index 000000000000..97260755da21
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/entry.server.tsx
@@ -0,0 +1,26 @@
+import { createReadableStreamFromReadable } from '@react-router/node';
+import * as Sentry from '@sentry/react-router';
+import { renderToPipeableStream } from 'react-dom/server';
+import { ServerRouter } from 'react-router';
+import { type HandleErrorFunction } from 'react-router';
+
+const ABORT_DELAY = 5_000;
+
+const handleRequest = Sentry.createSentryHandleRequest({
+ streamTimeout: ABORT_DELAY,
+ ServerRouter,
+ renderToPipeableStream,
+ createReadableStreamFromReadable,
+});
+
+export default handleRequest;
+
+export const handleError: HandleErrorFunction = (error, { request }) => {
+ // React Router may abort some interrupted requests, don't log those
+ if (!request.signal.aborted) {
+ Sentry.captureException(error);
+
+ // make sure to still log the error so you can see it
+ console.error(error);
+ }
+};
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/root.tsx
new file mode 100644
index 000000000000..227c08f7730c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/root.tsx
@@ -0,0 +1,69 @@
+import * as Sentry from '@sentry/react-router';
+import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
+import type { Route } from './+types/root';
+import stylesheet from './app.css?url';
+
+export const links: Route.LinksFunction = () => [
+ { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
+ {
+ rel: 'preconnect',
+ href: 'https://fonts.gstatic.com',
+ crossOrigin: 'anonymous',
+ },
+ {
+ rel: 'stylesheet',
+ href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
+ },
+ { rel: 'stylesheet', href: stylesheet },
+];
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ let message = 'Oops!';
+ let details = 'An unexpected error occurred.';
+ let stack: string | undefined;
+
+ if (isRouteErrorResponse(error)) {
+ message = error.status === 404 ? '404' : 'Error';
+ details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
+ } else if (error && error instanceof Error) {
+ Sentry.captureException(error);
+ if (import.meta.env.DEV) {
+ details = error.message;
+ stack = error.stack;
+ }
+ }
+
+ return (
+
+ {message}
+ {details}
+ {stack && (
+
+ {stack}
+
+ )}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes.ts
new file mode 100644
index 000000000000..b412893def52
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes.ts
@@ -0,0 +1,21 @@
+import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes';
+
+export default [
+ index('routes/home.tsx'),
+ ...prefix('errors', [
+ route('client', 'routes/errors/client.tsx'),
+ route('client/:client-param', 'routes/errors/client-param.tsx'),
+ route('client-loader', 'routes/errors/client-loader.tsx'),
+ route('server-loader', 'routes/errors/server-loader.tsx'),
+ route('client-action', 'routes/errors/client-action.tsx'),
+ route('server-action', 'routes/errors/server-action.tsx'),
+ ]),
+ ...prefix('performance', [
+ index('routes/performance/index.tsx'),
+ route('ssr', 'routes/performance/ssr.tsx'),
+ route('with/:param', 'routes/performance/dynamic-param.tsx'),
+ route('static', 'routes/performance/static.tsx'),
+ route('server-loader', 'routes/performance/server-loader.tsx'),
+ route('server-action', 'routes/performance/server-action.tsx'),
+ ]),
+] satisfies RouteConfig;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-action.tsx
new file mode 100644
index 000000000000..d3b2d08eef2e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-action.tsx
@@ -0,0 +1,18 @@
+import { Form } from 'react-router';
+
+export function clientAction() {
+ throw new Error('Madonna mia! Che casino nella Client Action!');
+}
+
+export default function ClientActionErrorPage() {
+ return (
+
+
Client Error Action Page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-loader.tsx
new file mode 100644
index 000000000000..72d9e62a99dc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-loader.tsx
@@ -0,0 +1,16 @@
+import type { Route } from './+types/server-loader';
+
+export function clientLoader() {
+ throw new Error('¡Madre mía del client loader!');
+ return { data: 'sad' };
+}
+
+export default function ClientLoaderErrorPage({ loaderData }: Route.ComponentProps) {
+ const { data } = loaderData;
+ return (
+
+
Client Loader Error Page
+
{data}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-param.tsx
new file mode 100644
index 000000000000..a2e423391f03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client-param.tsx
@@ -0,0 +1,17 @@
+import type { Route } from './+types/client-param';
+
+export default function ClientErrorParamPage({ params }: Route.ComponentProps) {
+ return (
+
+
Client Error Param Page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client.tsx
new file mode 100644
index 000000000000..190074a5ef09
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/client.tsx
@@ -0,0 +1,15 @@
+export default function ClientErrorPage() {
+ return (
+
+
Client Error Page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/server-action.tsx
new file mode 100644
index 000000000000..863c320f3557
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/server-action.tsx
@@ -0,0 +1,18 @@
+import { Form } from 'react-router';
+
+export function action() {
+ throw new Error('Madonna mia! Che casino nella Server Action!');
+}
+
+export default function ServerActionErrorPage() {
+ return (
+
+
Server Error Action Page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/server-loader.tsx
new file mode 100644
index 000000000000..cb777686d540
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/errors/server-loader.tsx
@@ -0,0 +1,16 @@
+import type { Route } from './+types/server-loader';
+
+export function loader() {
+ throw new Error('¡Madre mía del server!');
+ return { data: 'sad' };
+}
+
+export default function ServerLoaderErrorPage({ loaderData }: Route.ComponentProps) {
+ const { data } = loaderData;
+ return (
+
+
Server Error Page
+
{data}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/home.tsx
new file mode 100644
index 000000000000..4498e7a0d017
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/home.tsx
@@ -0,0 +1,9 @@
+import type { Route } from './+types/home';
+
+export function meta({}: Route.MetaArgs) {
+ return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
+}
+
+export default function Home() {
+ return home
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/dynamic-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/dynamic-param.tsx
new file mode 100644
index 000000000000..1ac02775f2ff
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/dynamic-param.tsx
@@ -0,0 +1,17 @@
+import type { Route } from './+types/dynamic-param';
+
+export async function loader() {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return { data: 'burritos' };
+}
+
+export default function DynamicParamPage({ params }: Route.ComponentProps) {
+ const { param } = params;
+
+ return (
+
+
Dynamic Parameter Page
+
The parameter value is: {param}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/index.tsx
new file mode 100644
index 000000000000..e5383306625a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/index.tsx
@@ -0,0 +1,14 @@
+import { Link } from 'react-router';
+
+export default function PerformancePage() {
+ return (
+
+
Performance Page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/server-action.tsx
new file mode 100644
index 000000000000..f149c5466b5a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/server-action.tsx
@@ -0,0 +1,25 @@
+import { Form } from 'react-router';
+import type { Route } from './+types/server-action';
+import * as Sentry from '@sentry/react-router';
+
+export const action = Sentry.wrapServerAction({}, async ({ request }: Route.ActionArgs) => {
+ let formData = await request.formData();
+ let name = formData.get('name');
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ return {
+ greeting: `Hola ${name}`,
+ };
+});
+
+export default function Project({ actionData }: Route.ComponentProps) {
+ return (
+
+
Server action page
+
+ {actionData ?
{actionData.greeting}
: null}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/server-loader.tsx
new file mode 100644
index 000000000000..da688d4dfe3e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/server-loader.tsx
@@ -0,0 +1,17 @@
+import type { Route } from './+types/server-loader';
+import * as Sentry from '@sentry/react-router';
+
+export const loader = Sentry.wrapServerLoader({}, async ({}: Route.LoaderArgs) => {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return { data: 'burritos' };
+});
+
+export default function ServerLoaderPage({ loaderData }: Route.ComponentProps) {
+ const { data } = loaderData;
+ return (
+
+
Server Loader Page
+
{data}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/ssr.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/ssr.tsx
new file mode 100644
index 000000000000..253e964ff15d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/ssr.tsx
@@ -0,0 +1,7 @@
+export default function SsrPage() {
+ return (
+
+
SSR Page
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/static.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/static.tsx
new file mode 100644
index 000000000000..3dea24381fdc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/app/routes/performance/static.tsx
@@ -0,0 +1,3 @@
+export default function StaticPage() {
+ return Static Page
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs
new file mode 100644
index 000000000000..a43afcba814f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/instrument.mjs
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/react-router';
+
+Sentry.init({
+ dsn: 'https://username@domain/123',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ tracesSampleRate: 1.0,
+ tunnel: `http://localhost:3031/`, // proxy server
+ integrations: function (integrations) {
+ return integrations.filter(integration => {
+ return integration.name !== 'ReactRouterServer';
+ });
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
new file mode 100644
index 000000000000..6f793c0d20eb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "react-router-7-framework-custom",
+ "version": "0.1.0",
+ "type": "module",
+ "private": true,
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router": "^7.1.5",
+ "@react-router/node": "^7.1.5",
+ "@react-router/serve": "^7.1.5",
+ "@sentry/react-router": "latest || *",
+ "@sentry-internal/feedback": "latest || *",
+ "@sentry-internal/replay-canvas": "latest || *",
+ "@sentry-internal/browser-utils": "latest || *",
+ "@sentry/browser": "latest || *",
+ "@sentry/core": "latest || *",
+ "@sentry/node": "latest || *",
+ "@sentry/opentelemetry": "latest || *",
+ "@sentry/react": "latest || *",
+ "@sentry-internal/replay": "latest || *",
+ "isbot": "^5.1.17"
+ },
+ "devDependencies": {
+ "@types/react": "18.3.1",
+ "@types/react-dom": "18.3.1",
+ "@types/node": "^20",
+ "@react-router/dev": "^7.1.5",
+ "@playwright/test": "~1.50.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.11"
+ },
+ "scripts": {
+ "build": "react-router build",
+ "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev",
+ "start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js",
+ "proxy": "node start-event-proxy.mjs",
+ "typecheck": "react-router typegen && tsc",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:ts && pnpm test:playwright",
+ "test:ts": "pnpm typecheck",
+ "test:playwright": "playwright test"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/playwright.config.mjs
new file mode 100644
index 000000000000..3ed5721107a7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `PORT=3030 pnpm start`,
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/public/favicon.ico
new file mode 100644
index 000000000000..5dbdfcddcb14
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/public/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/react-router.config.ts
new file mode 100644
index 000000000000..bb1f96469dd2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/react-router.config.ts
@@ -0,0 +1,6 @@
+import type { Config } from '@react-router/dev/config';
+
+export default {
+ ssr: true,
+ prerender: ['/performance/static'],
+} satisfies Config;
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/start-event-proxy.mjs
new file mode 100644
index 000000000000..fb8dabc7fcfa
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'react-router-7-framework-custom',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/constants.ts
new file mode 100644
index 000000000000..91653303b335
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/constants.ts
@@ -0,0 +1 @@
+export const APP_NAME = 'react-router-7-framework-custom';
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/errors/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/errors/errors.client.test.ts
new file mode 100644
index 000000000000..d6c80924c121
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/errors/errors.client.test.ts
@@ -0,0 +1,138 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('client-side errors', () => {
+ const errorMessage = '¡Madre mía!';
+ test('captures error thrown on click', async ({ page }) => {
+ const errorPromise = waitForError(APP_NAME, async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto(`/errors/client`);
+ await page.locator('#throw-on-click').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: '/errors/client',
+ request: {
+ url: expect.stringContaining('errors/client'),
+ headers: expect.any(Object),
+ },
+ level: 'error',
+ platform: 'javascript',
+ environment: 'qa',
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ },
+ tags: { runtime: 'browser' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ breadcrumbs: [
+ {
+ category: 'ui.click',
+ message: 'body > div > button#throw-on-click',
+ },
+ ],
+ });
+ });
+
+ test('captures error thrown on click from a parameterized route', async ({ page }) => {
+ const errorMessage = '¡Madre mía de churros!';
+ const errorPromise = waitForError(APP_NAME, async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto('/errors/client/churros');
+ await page.locator('#throw-on-click').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: '¡Madre mía de churros!',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ // todo: should be '/errors/client/:client-param'
+ transaction: '/errors/client/churros',
+ });
+ });
+
+ test('captures error thrown in a clientLoader', async ({ page }) => {
+ const errorMessage = '¡Madre mía del client loader!';
+ const errorPromise = waitForError(APP_NAME, async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto('/errors/client-loader');
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/errors/client-loader',
+ });
+ });
+
+ test('captures error thrown in a clientAction', async ({ page }) => {
+ const errorMessage = 'Madonna mia! Che casino nella Client Action!';
+ const errorPromise = waitForError(APP_NAME, async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto('/errors/client-action');
+ await page.locator('#submit').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: true,
+ },
+ },
+ ],
+ },
+ transaction: '/errors/client-action',
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/errors/errors.server.test.ts
new file mode 100644
index 000000000000..d702f8cee597
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/errors/errors.server.test.ts
@@ -0,0 +1,98 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('server-side errors', () => {
+ test('captures error thrown in server loader', async ({ page }) => {
+ const errorMessage = '¡Madre mía del server!';
+ const errorPromise = waitForError(APP_NAME, async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto(`/errors/server-loader`);
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: true,
+ },
+ },
+ ],
+ },
+ // todo: should be 'GET /errors/server-loader'
+ transaction: 'GET *',
+ request: {
+ url: expect.stringContaining('errors/server-loader'),
+ headers: expect.any(Object),
+ },
+ level: 'error',
+ platform: 'node',
+ environment: 'qa',
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ },
+ tags: { runtime: 'node' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ });
+ });
+
+ test('captures error thrown in server action', async ({ page }) => {
+ const errorMessage = 'Madonna mia! Che casino nella Server Action!';
+ const errorPromise = waitForError(APP_NAME, async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === errorMessage;
+ });
+
+ await page.goto(`/errors/server-action`);
+ await page.locator('#submit').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: errorMessage,
+ mechanism: {
+ handled: true,
+ },
+ },
+ ],
+ },
+ // todo: should be 'POST /errors/server-action'
+ transaction: 'POST *',
+ request: {
+ url: expect.stringContaining('errors/server-action'),
+ headers: expect.any(Object),
+ },
+ level: 'error',
+ platform: 'node',
+ environment: 'qa',
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ },
+ tags: { runtime: 'node' },
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ },
+ },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/navigation.client.test.ts
new file mode 100644
index 000000000000..57e3e764d6a8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/navigation.client.test.ts
@@ -0,0 +1,107 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('client - navigation performance', () => {
+ test('should create navigation transaction', async ({ page }) => {
+ const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === '/performance/ssr';
+ });
+
+ await page.goto(`/performance`); // pageload
+ await page.waitForTimeout(1000); // give it a sec before navigation
+ await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation
+
+ const transaction = await navigationPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.navigation.react-router',
+ 'sentry.op': 'navigation',
+ 'sentry.source': 'url',
+ },
+ op: 'navigation',
+ origin: 'auto.navigation.react-router',
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/performance/ssr',
+ type: 'transaction',
+ transaction_info: { source: 'url' },
+ platform: 'javascript',
+ request: {
+ url: expect.stringContaining('/performance/ssr'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: [
+ { name: 'npm:@sentry/react-router', version: expect.any(String) },
+ { name: 'npm:@sentry/browser', version: expect.any(String) },
+ ],
+ },
+ tags: { runtime: 'browser' },
+ });
+ });
+
+ test('should update navigation transaction for dynamic routes', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === '/performance/with/:param';
+ });
+
+ await page.goto(`/performance`); // pageload
+ await page.waitForTimeout(1000); // give it a sec before navigation
+ await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.navigation.react-router',
+ 'sentry.op': 'navigation',
+ 'sentry.source': 'route',
+ },
+ op: 'navigation',
+ origin: 'auto.navigation.react-router',
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/performance/with/:param',
+ type: 'transaction',
+ transaction_info: { source: 'route' },
+ platform: 'javascript',
+ request: {
+ url: expect.stringContaining('/performance/with/sentry'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: [
+ { name: 'npm:@sentry/react-router', version: expect.any(String) },
+ { name: 'npm:@sentry/browser', version: expect.any(String) },
+ ],
+ },
+ tags: { runtime: 'browser' },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/pageload.client.test.ts
new file mode 100644
index 000000000000..b18ae44e0e71
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/pageload.client.test.ts
@@ -0,0 +1,132 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('client - pageload performance', () => {
+ test('should send pageload transaction', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === '/performance/';
+ });
+
+ await page.goto(`/performance`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.pageload.browser',
+ 'sentry.op': 'pageload',
+ 'sentry.source': 'url',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/performance/',
+ type: 'transaction',
+ transaction_info: { source: 'url' },
+ measurements: expect.any(Object),
+ platform: 'javascript',
+ request: {
+ url: expect.stringContaining('/performance'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: [
+ { name: 'npm:@sentry/react-router', version: expect.any(String) },
+ { name: 'npm:@sentry/browser', version: expect.any(String) },
+ ],
+ },
+ tags: { runtime: 'browser' },
+ });
+ });
+
+ test('should update pageload transaction for dynamic routes', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === '/performance/with/:param';
+ });
+
+ await page.goto(`/performance/with/sentry`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.pageload.browser',
+ 'sentry.op': 'pageload',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/performance/with/:param',
+ type: 'transaction',
+ transaction_info: { source: 'route' },
+ measurements: expect.any(Object),
+ platform: 'javascript',
+ request: {
+ url: expect.stringContaining('/performance/with/sentry'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: [
+ { name: 'npm:@sentry/react-router', version: expect.any(String) },
+ { name: 'npm:@sentry/browser', version: expect.any(String) },
+ ],
+ },
+ tags: { runtime: 'browser' },
+ });
+ });
+
+ test('should send pageload transaction for prerendered pages', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === '/performance/static/';
+ });
+
+ await page.goto(`/performance/static`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ transaction: '/performance/static/',
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.pageload.browser',
+ 'sentry.op': 'pageload',
+ 'sentry.source': 'url',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ },
+ },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/performance.server.test.ts
new file mode 100644
index 000000000000..abca82a6d938
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/performance.server.test.ts
@@ -0,0 +1,163 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('servery - performance', () => {
+ test('should send server transaction on pageload', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === 'GET /performance';
+ });
+
+ await page.goto(`/performance`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.source': 'route',
+ },
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: 'GET /performance',
+ type: 'transaction',
+ transaction_info: { source: 'route' },
+ platform: 'node',
+ request: {
+ url: expect.stringContaining('/performance'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: [
+ { name: 'npm:@sentry/react-router', version: expect.any(String) },
+ { name: 'npm:@sentry/node', version: expect.any(String) },
+ ],
+ },
+ tags: {
+ runtime: 'node',
+ },
+ });
+ });
+
+ test('should send server transaction on parameterized route', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === 'GET /performance/with/:param';
+ });
+
+ await page.goto(`/performance/with/some-param`);
+
+ const transaction = await txPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.source': 'route',
+ },
+ op: 'http.server',
+ origin: 'auto.http.otel.http',
+ },
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: 'GET /performance/with/:param',
+ type: 'transaction',
+ transaction_info: { source: 'route' },
+ platform: 'node',
+ request: {
+ url: expect.stringContaining('/performance/with/some-param'),
+ headers: expect.any(Object),
+ },
+ event_id: expect.any(String),
+ environment: 'qa',
+ sdk: {
+ integrations: expect.arrayContaining([expect.any(String)]),
+ name: 'sentry.javascript.react-router',
+ version: expect.any(String),
+ packages: [
+ { name: 'npm:@sentry/react-router', version: expect.any(String) },
+ { name: 'npm:@sentry/node', version: expect.any(String) },
+ ],
+ },
+ tags: {
+ runtime: 'node',
+ },
+ });
+ });
+
+ test('should instrument wrapped server loader', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ console.log(110, transactionEvent.transaction);
+ return transactionEvent.transaction === 'GET /performance/server-loader';
+ });
+
+ await page.goto(`/performance`);
+ await page.waitForTimeout(500);
+ await page.getByRole('link', { name: 'Server Loader' }).click();
+
+ const transaction = await txPromise;
+
+ expect(transaction?.spans?.[transaction.spans?.length - 1]).toMatchObject({
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.http.react-router',
+ 'sentry.op': 'function.react-router.loader',
+ },
+ description: 'Executing Server Loader',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'function.react-router.loader',
+ origin: 'auto.http.react-router',
+ });
+ });
+
+ test('should instrument a wrapped server action', async ({ page }) => {
+ const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === 'POST /performance/server-action';
+ });
+
+ await page.goto(`/performance/server-action`);
+ await page.getByRole('button', { name: 'Submit' }).click();
+
+ const transaction = await txPromise;
+
+ expect(transaction?.spans?.[transaction.spans?.length - 1]).toMatchObject({
+ span_id: expect.any(String),
+ trace_id: expect.any(String),
+ data: {
+ 'sentry.origin': 'auto.http.react-router',
+ 'sentry.op': 'function.react-router.action',
+ },
+ description: 'Executing Server Action',
+ parent_span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'function.react-router.action',
+ origin: 'auto.http.react-router',
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/trace-propagation.test.ts
new file mode 100644
index 000000000000..7562297b2d4d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tests/performance/trace-propagation.test.ts
@@ -0,0 +1,43 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { APP_NAME } from '../constants';
+
+test.describe('Trace propagation', () => {
+ test('should inject metatags in ssr pageload', async ({ page }) => {
+ await page.goto(`/`);
+ const sentryTraceContent = await page.getAttribute('meta[name="sentry-trace"]', 'content');
+ expect(sentryTraceContent).toBeDefined();
+ expect(sentryTraceContent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[01]$/);
+ const baggageContent = await page.getAttribute('meta[name="baggage"]', 'content');
+ expect(baggageContent).toBeDefined();
+ expect(baggageContent).toContain('sentry-environment=qa');
+ expect(baggageContent).toContain('sentry-public_key=');
+ expect(baggageContent).toContain('sentry-trace_id=');
+ expect(baggageContent).toContain('sentry-transaction=');
+ expect(baggageContent).toContain('sentry-sampled=');
+ });
+
+ test('should have trace connection', async ({ page }) => {
+ const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === 'GET *';
+ });
+
+ const clientTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
+ return transactionEvent.transaction === '/';
+ });
+
+ await page.goto(`/`);
+ const serverTx = await serverTxPromise;
+ const clientTx = await clientTxPromise;
+
+ expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id);
+ expect(clientTx.contexts?.trace?.parent_span_id).toBe(serverTx.contexts?.trace?.span_id);
+ });
+
+ test('should not have trace connection for prerendered pages', async ({ page }) => {
+ await page.goto('/performance/static');
+
+ const sentryTraceElement = await page.$('meta[name="sentry-trace"]');
+ expect(sentryTraceElement).toBeNull();
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tsconfig.json
new file mode 100644
index 000000000000..1b510b528de9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["node", "vite/client"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "rootDirs": [".", "./.react-router/types"],
+ "baseUrl": ".",
+
+ "esModuleInterop": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true
+ },
+ "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
+ "exclude": ["tests/**/*"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/vite.config.ts
new file mode 100644
index 000000000000..68ba30d69397
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-custom/vite.config.ts
@@ -0,0 +1,6 @@
+import { reactRouter } from '@react-router/dev/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [reactRouter()],
+});
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index c5b446a70536..4f0cb5849c76 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -36,7 +36,7 @@
"@types/mongodb": "^3.6.20",
"@types/mysql": "^2.15.21",
"@types/pg": "^8.6.5",
- "ai": "^4.0.6",
+ "ai": "^4.3.16",
"amqplib": "^0.10.7",
"apollo-server": "^3.11.1",
"body-parser": "^1.20.3",
diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts
index 693a22baef59..8f594e449162 100644
--- a/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts
+++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts
@@ -25,6 +25,13 @@ app.get('/test/isolationScope', () => {
throw new Error('isolation_test_error');
});
+app.get('/test/withIsolationScope', () => {
+ Sentry.withIsolationScope(iScope => {
+ iScope.setTag('with-isolation-scope', 'tag');
+ throw new Error('with_isolation_scope_test_error');
+ });
+});
+
Sentry.setupExpressErrorHandler(app);
startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts
index bd2f51c16dbd..306449b09569 100644
--- a/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts
+++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts
@@ -86,3 +86,49 @@ test('isolation scope is applied to thrown error caught by global handler', asyn
runner.makeRequest('get', '/test/isolationScope', { expectError: true });
await runner.completed();
});
+
+/**
+ * This test shows that an inner isolation scope, created via `withIsolationScope`, is not applied to the error.
+ *
+ * This behaviour occurs because, just like in the test above where we use `getIsolationScope().setTag`,
+ * this isolation scope again is only valid as long as we're in the callback.
+ *
+ * So why _does_ the http isolation scope get applied then? Because express' error handler applies on
+ * a per-request basis, meaning, it's called while we're inside the isolation scope of the http request,
+ * created from our `httpIntegration`.
+ */
+test('withIsolationScope scope is NOT applied to thrown error caught by global handler', async () => {
+ const runner = createRunner(__dirname, 'server.ts')
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ mechanism: {
+ type: 'middleware',
+ handled: false,
+ },
+ type: 'Error',
+ value: 'with_isolation_scope_test_error',
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ function: expect.any(String),
+ lineno: expect.any(Number),
+ colno: expect.any(Number),
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+ // 'with-isolation-scope' tag is not applied to the event
+ tags: expect.not.objectContaining({ 'with-isolation-scope': expect.anything() }),
+ },
+ })
+ .start();
+
+ runner.makeRequest('get', '/test/withIsolationScope', { expectError: true });
+
+ await runner.completed();
+});
diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts
index 693a22baef59..8f594e449162 100644
--- a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts
+++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/server.ts
@@ -25,6 +25,13 @@ app.get('/test/isolationScope', () => {
throw new Error('isolation_test_error');
});
+app.get('/test/withIsolationScope', () => {
+ Sentry.withIsolationScope(iScope => {
+ iScope.setTag('with-isolation-scope', 'tag');
+ throw new Error('with_isolation_scope_test_error');
+ });
+});
+
Sentry.setupExpressErrorHandler(app);
startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts
index eda622f1cf6c..61291f86320d 100644
--- a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts
+++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts
@@ -53,7 +53,7 @@ test('withScope scope is NOT applied to thrown error caught by global handler',
/**
* This test shows that the isolation scope set tags are applied correctly to the error.
*/
-test('isolation scope is applied to thrown error caught by global handler', async () => {
+test('http requestisolation scope is applied to thrown error caught by global handler', async () => {
const runner = createRunner(__dirname, 'server.ts')
.expect({
event: {
@@ -90,3 +90,49 @@ test('isolation scope is applied to thrown error caught by global handler', asyn
await runner.completed();
});
+
+/**
+ * This test shows that an inner isolation scope, created via `withIsolationScope`, is not applied to the error.
+ *
+ * This behaviour occurs because, just like in the test above where we use `getIsolationScope().setTag`,
+ * this isolation scope again is only valid as long as we're in the callback.
+ *
+ * So why _does_ the http isolation scope get applied then? Because express' error handler applies on
+ * a per-request basis, meaning, it's called while we're inside the isolation scope of the http request,
+ * created from our `httpIntegration`.
+ */
+test('withIsolationScope scope is NOT applied to thrown error caught by global handler', async () => {
+ const runner = createRunner(__dirname, 'server.ts')
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ mechanism: {
+ type: 'middleware',
+ handled: false,
+ },
+ type: 'Error',
+ value: 'with_isolation_scope_test_error',
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ function: expect.any(String),
+ lineno: expect.any(Number),
+ colno: expect.any(Number),
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+ // 'with-isolation-scope' tag is not applied to the event
+ tags: expect.not.objectContaining({ 'with-isolation-scope': expect.anything() }),
+ },
+ })
+ .start();
+
+ runner.makeRequest('get', '/test/withIsolationScope', { expectError: true });
+
+ await runner.completed();
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/ai/test.ts b/dev-packages/node-integration-tests/suites/tracing/ai/test.ts
deleted file mode 100644
index c0a3ccb4a78a..000000000000
--- a/dev-packages/node-integration-tests/suites/tracing/ai/test.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import { afterAll, describe, expect } from 'vitest';
-import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
-
-// `ai` SDK only support Node 18+
-describe('ai', () => {
- afterAll(() => {
- cleanupChildProcesses();
- });
-
- const EXPECTED_TRANSACTION = {
- transaction: 'main',
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: expect.objectContaining({
- 'ai.completion_tokens.used': 20,
- 'ai.model.id': 'mock-model-id',
- 'ai.model.provider': 'mock-provider',
- 'ai.model_id': 'mock-model-id',
- 'ai.operationId': 'ai.generateText',
- 'ai.pipeline.name': 'generateText',
- 'ai.prompt_tokens.used': 10,
- 'ai.response.finishReason': 'stop',
- 'ai.settings.maxRetries': 2,
- 'ai.settings.maxSteps': 1,
- 'ai.streaming': false,
- 'ai.total_tokens.used': 30,
- 'ai.usage.completionTokens': 20,
- 'ai.usage.promptTokens': 10,
- 'operation.name': 'ai.generateText',
- 'sentry.op': 'ai.pipeline.generateText',
- 'sentry.origin': 'auto.vercelai.otel',
- }),
- description: 'generateText',
- op: 'ai.pipeline.generateText',
- origin: 'auto.vercelai.otel',
- status: 'ok',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.origin': 'auto.vercelai.otel',
- 'sentry.op': 'ai.run.doGenerate',
- 'operation.name': 'ai.generateText.doGenerate',
- 'ai.operationId': 'ai.generateText.doGenerate',
- 'ai.model.provider': 'mock-provider',
- 'ai.model.id': 'mock-model-id',
- 'ai.settings.maxRetries': 2,
- 'gen_ai.system': 'mock-provider',
- 'gen_ai.request.model': 'mock-model-id',
- 'ai.pipeline.name': 'generateText.doGenerate',
- 'ai.model_id': 'mock-model-id',
- 'ai.streaming': false,
- 'ai.response.finishReason': 'stop',
- 'ai.response.model': 'mock-model-id',
- 'ai.usage.promptTokens': 10,
- 'ai.usage.completionTokens': 20,
- 'gen_ai.response.finish_reasons': ['stop'],
- 'gen_ai.usage.input_tokens': 10,
- 'gen_ai.usage.output_tokens': 20,
- 'ai.completion_tokens.used': 20,
- 'ai.prompt_tokens.used': 10,
- 'ai.total_tokens.used': 30,
- }),
- description: 'generateText.doGenerate',
- op: 'ai.run.doGenerate',
- origin: 'auto.vercelai.otel',
- status: 'ok',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'ai.completion_tokens.used': 20,
- 'ai.model.id': 'mock-model-id',
- 'ai.model.provider': 'mock-provider',
- 'ai.model_id': 'mock-model-id',
- 'ai.prompt': '{"prompt":"Where is the second span?"}',
- 'ai.operationId': 'ai.generateText',
- 'ai.pipeline.name': 'generateText',
- 'ai.prompt_tokens.used': 10,
- 'ai.response.finishReason': 'stop',
- 'ai.input_messages': '{"prompt":"Where is the second span?"}',
- 'ai.settings.maxRetries': 2,
- 'ai.settings.maxSteps': 1,
- 'ai.streaming': false,
- 'ai.total_tokens.used': 30,
- 'ai.usage.completionTokens': 20,
- 'ai.usage.promptTokens': 10,
- 'operation.name': 'ai.generateText',
- 'sentry.op': 'ai.pipeline.generateText',
- 'sentry.origin': 'auto.vercelai.otel',
- }),
- description: 'generateText',
- op: 'ai.pipeline.generateText',
- origin: 'auto.vercelai.otel',
- status: 'ok',
- }),
- expect.objectContaining({
- data: expect.objectContaining({
- 'sentry.origin': 'auto.vercelai.otel',
- 'sentry.op': 'ai.run.doGenerate',
- 'operation.name': 'ai.generateText.doGenerate',
- 'ai.operationId': 'ai.generateText.doGenerate',
- 'ai.model.provider': 'mock-provider',
- 'ai.model.id': 'mock-model-id',
- 'ai.settings.maxRetries': 2,
- 'gen_ai.system': 'mock-provider',
- 'gen_ai.request.model': 'mock-model-id',
- 'ai.pipeline.name': 'generateText.doGenerate',
- 'ai.model_id': 'mock-model-id',
- 'ai.streaming': false,
- 'ai.response.finishReason': 'stop',
- 'ai.response.model': 'mock-model-id',
- 'ai.usage.promptTokens': 10,
- 'ai.usage.completionTokens': 20,
- 'gen_ai.response.finish_reasons': ['stop'],
- 'gen_ai.usage.input_tokens': 10,
- 'gen_ai.usage.output_tokens': 20,
- 'ai.completion_tokens.used': 20,
- 'ai.prompt_tokens.used': 10,
- 'ai.total_tokens.used': 30,
- }),
- description: 'generateText.doGenerate',
- op: 'ai.run.doGenerate',
- origin: 'auto.vercelai.otel',
- status: 'ok',
- }),
- ]),
- };
-
- createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
- test('creates ai related spans ', async () => {
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
- });
- });
-});
diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs
new file mode 100644
index 000000000000..b798e21228f5
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs
@@ -0,0 +1,11 @@
+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,
+ sendDefaultPii: true,
+ transport: loggingTransport,
+ integrations: [Sentry.vercelAIIntegration()],
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/ai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs
similarity index 84%
rename from dev-packages/node-integration-tests/suites/tracing/ai/instrument.mjs
rename to dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs
index 46a27dd03b74..5e898ee1949d 100644
--- a/dev-packages/node-integration-tests/suites/tracing/ai/instrument.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs
@@ -6,4 +6,5 @@ Sentry.init({
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
+ integrations: [Sentry.vercelAIIntegration()],
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/ai/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs
similarity index 100%
rename from dev-packages/node-integration-tests/suites/tracing/ai/scenario.mjs
rename to dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs
diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts
new file mode 100644
index 000000000000..7876dbccb440
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts
@@ -0,0 +1,267 @@
+import { afterAll, describe, expect } from 'vitest';
+import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
+
+// `ai` SDK only support Node 18+
+describe('Vercel AI integration', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // First span - no telemetry config, should enable telemetry but not record inputs/outputs when sendDefaultPii: false
+ expect.objectContaining({
+ data: {
+ 'ai.model.id': 'mock-model-id',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.operationId': 'ai.generateText',
+ 'ai.pipeline.name': 'generateText',
+ 'ai.response.finishReason': 'stop',
+ 'ai.settings.maxRetries': 2,
+ 'ai.settings.maxSteps': 1,
+ 'ai.streaming': false,
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'ai.pipeline.generateText',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'ai.pipeline.generateText',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Second span - explicitly enabled telemetry but recordInputs/recordOutputs not set, should not record when sendDefaultPii: false
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'ai.run.doGenerate',
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'ai.operationId': 'ai.generateText.doGenerate',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.model.id': 'mock-model-id',
+ 'ai.settings.maxRetries': 2,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ 'ai.pipeline.name': 'generateText.doGenerate',
+ 'ai.streaming': false,
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.model': 'mock-model-id',
+ 'ai.response.id': expect.any(String),
+ 'ai.response.timestamp': expect.any(String),
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.total_tokens': 30,
+ },
+ description: 'generateText.doGenerate',
+ op: 'ai.run.doGenerate',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Third span - explicit telemetry enabled, should record inputs/outputs regardless of sendDefaultPii
+ expect.objectContaining({
+ data: {
+ 'ai.model.id': 'mock-model-id',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.operationId': 'ai.generateText',
+ 'ai.pipeline.name': 'generateText',
+ 'ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.text': expect.any(String),
+ 'ai.settings.maxRetries': 2,
+ 'ai.settings.maxSteps': 1,
+ 'ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'ai.pipeline.generateText',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'ai.pipeline.generateText',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Fourth span - doGenerate for explicit telemetry enabled call
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'ai.run.doGenerate',
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'ai.operationId': 'ai.generateText.doGenerate',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.model.id': 'mock-model-id',
+ 'ai.settings.maxRetries': 2,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ 'ai.pipeline.name': 'generateText.doGenerate',
+ 'ai.streaming': false,
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.model': 'mock-model-id',
+ 'ai.response.id': expect.any(String),
+ 'ai.response.text': expect.any(String),
+ 'ai.response.timestamp': expect.any(String),
+ 'ai.prompt.format': expect.any(String),
+ 'ai.prompt.messages': expect.any(String),
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.total_tokens': 30,
+ },
+ description: 'generateText.doGenerate',
+ op: 'ai.run.doGenerate',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ ]),
+ };
+
+ const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // First span - no telemetry config, should enable telemetry AND record inputs/outputs when sendDefaultPii: true
+ expect.objectContaining({
+ data: {
+ 'ai.model.id': 'mock-model-id',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.operationId': 'ai.generateText',
+ 'ai.pipeline.name': 'generateText',
+ 'ai.prompt': '{"prompt":"Where is the first span?"}',
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.text': 'First span here!',
+ 'ai.settings.maxRetries': 2,
+ 'ai.settings.maxSteps': 1,
+ 'ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"Where is the first span?"}',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'ai.pipeline.generateText',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'ai.pipeline.generateText',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Second span - doGenerate for first call, should also include input/output fields when sendDefaultPii: true
+ expect.objectContaining({
+ data: {
+ 'ai.model.id': 'mock-model-id',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.operationId': 'ai.generateText.doGenerate',
+ 'ai.pipeline.name': 'generateText.doGenerate',
+ 'ai.prompt.format': 'prompt',
+ 'ai.prompt.messages': '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]',
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.id': expect.any(String),
+ 'ai.response.model': 'mock-model-id',
+ 'ai.response.text': 'First span here!',
+ 'ai.response.timestamp': expect.any(String),
+ 'ai.settings.maxRetries': 2,
+ 'ai.streaming': false,
+ 'gen_ai.request.model': 'mock-model-id',
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'sentry.op': 'ai.run.doGenerate',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText.doGenerate',
+ op: 'ai.run.doGenerate',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Third span - explicitly enabled telemetry, should record inputs/outputs regardless of sendDefaultPii
+ expect.objectContaining({
+ data: {
+ 'ai.model.id': 'mock-model-id',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.operationId': 'ai.generateText',
+ 'ai.pipeline.name': 'generateText',
+ 'ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.text': expect.any(String),
+ 'ai.settings.maxRetries': 2,
+ 'ai.settings.maxSteps': 1,
+ 'ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'ai.pipeline.generateText',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'ai.pipeline.generateText',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Fourth span - doGenerate for explicitly enabled telemetry call
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'ai.run.doGenerate',
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'ai.operationId': 'ai.generateText.doGenerate',
+ 'ai.model.provider': 'mock-provider',
+ 'ai.model.id': 'mock-model-id',
+ 'ai.settings.maxRetries': 2,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ 'ai.pipeline.name': 'generateText.doGenerate',
+ 'ai.streaming': false,
+ 'ai.response.finishReason': 'stop',
+ 'ai.response.model': 'mock-model-id',
+ 'ai.response.id': expect.any(String),
+ 'ai.response.text': expect.any(String),
+ 'ai.response.timestamp': expect.any(String),
+ 'ai.prompt.format': expect.any(String),
+ 'ai.prompt.messages': expect.any(String),
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.total_tokens': 30,
+ },
+ description: 'generateText.doGenerate',
+ op: 'ai.run.doGenerate',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ ]),
+ };
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('creates ai related spans with sendDefaultPii: false', async () => {
+ await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed();
+ });
+ });
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
+ test('creates ai related spans with sendDefaultPii: true', async () => {
+ await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed();
+ });
+ });
+});
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 29105cfb4b18..ac222eca825b 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -134,6 +134,7 @@ export {
logger,
consoleLoggingIntegration,
wrapMcpServerWithSentry,
+ NODE_VERSION,
} from '@sentry/node';
export { init } from './server/sdk';
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index 942951c165da..24513a325188 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -120,6 +120,7 @@ export {
logger,
consoleLoggingIntegration,
wrapMcpServerWithSentry,
+ NODE_VERSION,
} from '@sentry/node';
export {
diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts
index 05b7d7ed17a8..30a628b5997f 100644
--- a/packages/browser-utils/src/metrics/inp.ts
+++ b/packages/browser-utils/src/metrics/inp.ts
@@ -12,6 +12,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
+import type { InstrumentationHandlerCallback } from './instrument';
import {
addInpInstrumentationHandler,
addPerformanceInstrumentationHandler,
@@ -22,6 +23,11 @@ import { getBrowserPerformanceAPI, msToSec, startStandaloneWebVitalSpan } from '
const LAST_INTERACTIONS: number[] = [];
const INTERACTIONS_SPAN_MAP = new Map();
+/**
+ * 60 seconds is the maximum for a plausible INP value
+ * (source: Me)
+ */
+const MAX_PLAUSIBLE_INP_DURATION = 60;
/**
* Start tracking INP webvital events.
*/
@@ -67,62 +73,77 @@ const INP_ENTRY_MAP: Record = {
input: 'press',
};
-/** Starts tracking the Interaction to Next Paint on the current page. */
-function _trackINP(): () => void {
- return addInpInstrumentationHandler(({ metric }) => {
- if (metric.value == undefined) {
- return;
- }
+/** Starts tracking the Interaction to Next Paint on the current page. #
+ * exported only for testing
+ */
+export function _trackINP(): () => void {
+ return addInpInstrumentationHandler(_onInp);
+}
+
+/**
+ * exported only for testing
+ */
+export const _onInp: InstrumentationHandlerCallback = ({ metric }) => {
+ if (metric.value == undefined) {
+ return;
+ }
- const entry = metric.entries.find(entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name]);
+ const duration = msToSec(metric.value);
- if (!entry) {
- return;
- }
+ // We received occasional reports of hour-long INP values.
+ // Therefore, we add a sanity check to avoid creating spans for
+ // unrealistically long INP durations.
+ if (duration > MAX_PLAUSIBLE_INP_DURATION) {
+ return;
+ }
- const { interactionId } = entry;
- const interactionType = INP_ENTRY_MAP[entry.name];
+ const entry = metric.entries.find(entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name]);
- /** Build the INP span, create an envelope from the span, and then send the envelope */
- const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime);
- const duration = msToSec(metric.value);
- const activeSpan = getActiveSpan();
- const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
+ if (!entry) {
+ return;
+ }
- // We first try to lookup the span from our INTERACTIONS_SPAN_MAP,
- // where we cache the route per interactionId
- const cachedSpan = interactionId != null ? INTERACTIONS_SPAN_MAP.get(interactionId) : undefined;
+ const { interactionId } = entry;
+ const interactionType = INP_ENTRY_MAP[entry.name];
- const spanToUse = cachedSpan || rootSpan;
+ /** Build the INP span, create an envelope from the span, and then send the envelope */
+ const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime);
+ const activeSpan = getActiveSpan();
+ const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
- // Else, we try to use the active span.
- // Finally, we fall back to look at the transactionName on the scope
- const routeName = spanToUse ? spanToJSON(spanToUse).description : getCurrentScope().getScopeData().transactionName;
+ // We first try to lookup the span from our INTERACTIONS_SPAN_MAP,
+ // where we cache the route per interactionId
+ const cachedSpan = interactionId != null ? INTERACTIONS_SPAN_MAP.get(interactionId) : undefined;
- const name = htmlTreeAsString(entry.target);
- const attributes: SpanAttributes = {
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.inp',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `ui.interaction.${interactionType}`,
- [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry.duration,
- };
+ const spanToUse = cachedSpan || rootSpan;
- const span = startStandaloneWebVitalSpan({
- name,
- transaction: routeName,
- attributes,
- startTime,
- });
+ // Else, we try to use the active span.
+ // Finally, we fall back to look at the transactionName on the scope
+ const routeName = spanToUse ? spanToJSON(spanToUse).description : getCurrentScope().getScopeData().transactionName;
- if (span) {
- span.addEvent('inp', {
- [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
- [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value,
- });
+ const name = htmlTreeAsString(entry.target);
+ const attributes: SpanAttributes = {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.inp',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `ui.interaction.${interactionType}`,
+ [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry.duration,
+ };
- span.end(startTime + duration);
- }
+ const span = startStandaloneWebVitalSpan({
+ name,
+ transaction: routeName,
+ attributes,
+ startTime,
});
-}
+
+ if (span) {
+ span.addEvent('inp', {
+ [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
+ [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value,
+ });
+
+ span.end(startTime + duration);
+ }
+};
/**
* Register a listener to cache route information for INP interactions.
diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts
index 7b9d7e562f37..cb84908ce55b 100644
--- a/packages/browser-utils/src/metrics/instrument.ts
+++ b/packages/browser-utils/src/metrics/instrument.ts
@@ -158,13 +158,17 @@ export function addTtfbInstrumentationHandler(callback: (data: { metric: Metric
return addMetricObserver('ttfb', callback, instrumentTtfb, _previousTtfb);
}
+export type InstrumentationHandlerCallback = (data: {
+ metric: Omit & {
+ entries: PerformanceEventTiming[];
+ };
+}) => void;
+
/**
* Add a callback that will be triggered when a INP metric is available.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
*/
-export function addInpInstrumentationHandler(
- callback: (data: { metric: Omit & { entries: PerformanceEventTiming[] } }) => void,
-): CleanupHandlerCallback {
+export function addInpInstrumentationHandler(callback: InstrumentationHandlerCallback): CleanupHandlerCallback {
return addMetricObserver('inp', callback, instrumentInp, _previousInp);
}
diff --git a/packages/browser-utils/test/instrument/metrics/inpt.test.ts b/packages/browser-utils/test/instrument/metrics/inpt.test.ts
new file mode 100644
index 000000000000..437ae650d0fe
--- /dev/null
+++ b/packages/browser-utils/test/instrument/metrics/inpt.test.ts
@@ -0,0 +1,116 @@
+import { afterEach } from 'node:test';
+import { describe, expect, it, vi } from 'vitest';
+import { _onInp, _trackINP } from '../../../src/metrics/inp';
+import * as instrument from '../../../src/metrics/instrument';
+import * as utils from '../../../src/metrics/utils';
+
+describe('_trackINP', () => {
+ const addInpInstrumentationHandler = vi.spyOn(instrument, 'addInpInstrumentationHandler');
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('adds an instrumentation handler', () => {
+ _trackINP();
+ expect(addInpInstrumentationHandler).toHaveBeenCalledOnce();
+ });
+
+ it('returns an unsubscribe dunction', () => {
+ const handler = _trackINP();
+ expect(typeof handler).toBe('function');
+ });
+});
+
+describe('_onInp', () => {
+ it('early-returns if the INP metric entry has no value', () => {
+ const startStandaloneWebVitalSpanSpy = vi.spyOn(utils, 'startStandaloneWebVitalSpan');
+
+ const metric = {
+ value: undefined,
+ entries: [],
+ };
+ // @ts-expect-error - incomplete metric object
+ _onInp({ metric });
+
+ expect(startStandaloneWebVitalSpanSpy).not.toHaveBeenCalled();
+ });
+
+ it('early-returns if the INP metric value is greater than 60 seconds', () => {
+ const startStandaloneWebVitalSpanSpy = vi.spyOn(utils, 'startStandaloneWebVitalSpan');
+
+ const metric = {
+ value: 60_001,
+ entries: [
+ { name: 'click', duration: 60_001, interactionId: 1 },
+ { name: 'click', duration: 60_000, interactionId: 2 },
+ ],
+ };
+ // @ts-expect-error - incomplete metric object
+ _onInp({ metric });
+
+ expect(startStandaloneWebVitalSpanSpy).not.toHaveBeenCalled();
+ });
+
+ it('early-returns if the inp metric has an unknown interaction type', () => {
+ const startStandaloneWebVitalSpanSpy = vi.spyOn(utils, 'startStandaloneWebVitalSpan');
+
+ const metric = {
+ value: 10,
+ entries: [{ name: 'unknown', duration: 10, interactionId: 1 }],
+ };
+ // @ts-expect-error - incomplete metric object
+ _onInp({ metric });
+
+ expect(startStandaloneWebVitalSpanSpy).not.toHaveBeenCalled();
+ });
+
+ it('starts a span for a valid INP metric entry', () => {
+ const startStandaloneWebVitalSpanSpy = vi.spyOn(utils, 'startStandaloneWebVitalSpan');
+
+ const metric = {
+ value: 10,
+ entries: [{ name: 'click', duration: 10, interactionId: 1 }],
+ };
+ // @ts-expect-error - incomplete metric object
+ _onInp({ metric });
+
+ expect(startStandaloneWebVitalSpanSpy).toHaveBeenCalledTimes(1);
+ expect(startStandaloneWebVitalSpanSpy).toHaveBeenCalledWith({
+ attributes: {
+ 'sentry.exclusive_time': 10,
+ 'sentry.op': 'ui.interaction.click',
+ 'sentry.origin': 'auto.http.browser.inp',
+ },
+ name: '',
+ startTime: NaN,
+ transaction: undefined,
+ });
+ });
+
+ it('takes the correct entry based on the metric value', () => {
+ const startStandaloneWebVitalSpanSpy = vi.spyOn(utils, 'startStandaloneWebVitalSpan');
+
+ const metric = {
+ value: 10,
+ entries: [
+ { name: 'click', duration: 10, interactionId: 1 },
+ { name: 'click', duration: 9, interactionId: 2 },
+ ],
+ };
+ // @ts-expect-error - incomplete metric object
+ _onInp({ metric });
+
+ expect(startStandaloneWebVitalSpanSpy).toHaveBeenCalledTimes(1);
+ expect(startStandaloneWebVitalSpanSpy).toHaveBeenCalledWith({
+ attributes: {
+ 'sentry.exclusive_time': 10,
+ 'sentry.op': 'ui.interaction.click',
+ 'sentry.origin': 'auto.http.browser.inp',
+ },
+ name: '',
+ startTime: NaN,
+ transaction: undefined,
+ });
+ });
+});
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index f5d593312743..f2622e591497 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -120,6 +120,7 @@ export {
logger,
consoleLoggingIntegration,
wrapMcpServerWithSentry,
+ NODE_VERSION,
} from '@sentry/node';
export {
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 5a933002bc23..589937b21fd4 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -54,6 +54,7 @@ export { createGetModuleFromFilename } from './utils/module';
export { makeNodeTransport } from './transports';
export { NodeClient } from './sdk/client';
export { cron } from './cron';
+export { NODE_VERSION } from './nodeVersion';
export type { NodeOptions } from './types';
diff --git a/packages/node/src/integrations/tracing/vercelai/attributes.ts b/packages/node/src/integrations/tracing/vercelai/attributes.ts
new file mode 100644
index 000000000000..8d7b6913a636
--- /dev/null
+++ b/packages/node/src/integrations/tracing/vercelai/attributes.ts
@@ -0,0 +1,794 @@
+/**
+ * AI SDK Telemetry Attributes
+ * Based on https://ai-sdk.dev/docs/ai-sdk-core/telemetry#collected-data
+ */
+
+// =============================================================================
+// COMMON ATTRIBUTES
+// =============================================================================
+
+/**
+ * Common attribute for operation name across all functions and spans
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#collected-data
+ */
+export const OPERATION_NAME_ATTRIBUTE = 'operation.name';
+
+/**
+ * Common attribute for AI operation ID across all functions and spans
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#collected-data
+ */
+export const AI_OPERATION_ID_ATTRIBUTE = 'ai.operationId';
+
+// =============================================================================
+// SHARED ATTRIBUTES
+// =============================================================================
+
+/**
+ * `generateText` function - `ai.generateText` span
+ * `streamText` function - `ai.streamText` span
+ *
+ * The prompt that was used when calling the function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamtext-function
+ */
+export const AI_PROMPT_ATTRIBUTE = 'ai.prompt';
+
+/**
+ * `generateObject` function - `ai.generateObject` span
+ * `streamObject` function - `ai.streamObject` span
+ *
+ * The JSON schema version of the schema that was passed into the function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_SCHEMA_ATTRIBUTE = 'ai.schema';
+
+/**
+ * `generateObject` function - `ai.generateObject` span
+ * `streamObject` function - `ai.streamObject` span
+ *
+ * The name of the schema that was passed into the function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_SCHEMA_NAME_ATTRIBUTE = 'ai.schema.name';
+
+/**
+ * `generateObject` function - `ai.generateObject` span
+ * `streamObject` function - `ai.streamObject` span
+ *
+ * The description of the schema that was passed into the function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_SCHEMA_DESCRIPTION_ATTRIBUTE = 'ai.schema.description';
+
+/**
+ * `generateObject` function - `ai.generateObject` span
+ * `streamObject` function - `ai.streamObject` span
+ *
+ * The object that was generated (stringified JSON)
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_RESPONSE_OBJECT_ATTRIBUTE = 'ai.response.object';
+
+/**
+ * `generateObject` function - `ai.generateObject` span
+ * `streamObject` function - `ai.streamObject` span
+ *
+ * The object generation mode, e.g. `json`
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_SETTINGS_MODE_ATTRIBUTE = 'ai.settings.mode';
+
+/**
+ * `generateObject` function - `ai.generateObject` span
+ * `streamObject` function - `ai.streamObject` span
+ *
+ * The output type that was used, e.g. `object` or `no-schema`
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_SETTINGS_OUTPUT_ATTRIBUTE = 'ai.settings.output';
+
+/**
+ * `embed` function - `ai.embed.doEmbed` span
+ * `embedMany` function - `ai.embedMany` span
+ *
+ * The values that were passed into the function (array)
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embed-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embedmany-function
+ */
+export const AI_VALUES_ATTRIBUTE = 'ai.values';
+
+/**
+ * `embed` function - `ai.embed.doEmbed` span
+ * `embedMany` function - `ai.embedMany` span
+ *
+ * An array of JSON-stringified embeddings
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embed-function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embedmany-function
+ */
+export const AI_EMBEDDINGS_ATTRIBUTE = 'ai.embeddings';
+
+// =============================================================================
+// GENERATETEXT FUNCTION - UNIQUE ATTRIBUTES
+// =============================================================================
+
+/**
+ * `generateText` function - `ai.generateText` span
+ *
+ * The text that was generated
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_RESPONSE_TEXT_ATTRIBUTE = 'ai.response.text';
+
+/**
+ * `generateText` function - `ai.generateText` span
+ *
+ * The tool calls that were made as part of the generation (stringified JSON)
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_RESPONSE_TOOL_CALLS_ATTRIBUTE = 'ai.response.toolCalls';
+
+/**
+ * `generateText` function - `ai.generateText` span
+ *
+ * The reason why the generation finished
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_RESPONSE_FINISH_REASON_ATTRIBUTE = 'ai.response.finishReason';
+
+/**
+ * `generateText` function - `ai.generateText` span
+ *
+ * The maximum number of steps that were set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_SETTINGS_MAX_STEPS_ATTRIBUTE = 'ai.settings.maxSteps';
+
+/**
+ * `generateText` function - `ai.generateText.doGenerate` span
+ *
+ * The format of the prompt
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_PROMPT_FORMAT_ATTRIBUTE = 'ai.prompt.format';
+
+/**
+ * `generateText` function - `ai.generateText.doGenerate` span
+ *
+ * The messages that were passed into the provider
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_PROMPT_MESSAGES_ATTRIBUTE = 'ai.prompt.messages';
+
+/**
+ * `generateText` function - `ai.generateText.doGenerate` span
+ *
+ * Array of stringified tool definitions
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_PROMPT_TOOLS_ATTRIBUTE = 'ai.prompt.tools';
+
+/**
+ * `generateText` function - `ai.generateText.doGenerate` span
+ *
+ * The stringified tool choice setting (JSON)
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_PROMPT_TOOL_CHOICE_ATTRIBUTE = 'ai.prompt.toolChoice';
+
+// =============================================================================
+// STREAMTEXT FUNCTION - UNIQUE ATTRIBUTES
+// =============================================================================
+
+/**
+ * `streamText` function - `ai.streamText.doStream` span
+ *
+ * The time it took to receive the first chunk in milliseconds
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamtext-function
+ */
+export const AI_RESPONSE_MS_TO_FIRST_CHUNK_ATTRIBUTE = 'ai.response.msToFirstChunk';
+
+/**
+ * `streamText` function - `ai.streamText.doStream` span
+ *
+ * The time it took to receive the finish part of the LLM stream in milliseconds
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamtext-function
+ */
+export const AI_RESPONSE_MS_TO_FINISH_ATTRIBUTE = 'ai.response.msToFinish';
+
+/**
+ * `streamText` function - `ai.streamText.doStream` span
+ *
+ * The average completion tokens per second
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamtext-function
+ */
+export const AI_RESPONSE_AVG_COMPLETION_TOKENS_PER_SECOND_ATTRIBUTE = 'ai.response.avgCompletionTokensPerSecond';
+
+// =============================================================================
+// EMBED FUNCTION - UNIQUE ATTRIBUTES
+// =============================================================================
+
+/**
+ * `embed` function - `ai.embed` span
+ *
+ * The value that was passed into the `embed` function
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embed-function
+ */
+export const AI_VALUE_ATTRIBUTE = 'ai.value';
+
+/**
+ * `embed` function - `ai.embed` span
+ *
+ * A JSON-stringified embedding
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embed-function
+ */
+export const AI_EMBEDDING_ATTRIBUTE = 'ai.embedding';
+
+// =============================================================================
+// BASIC LLM SPAN INFORMATION
+// =============================================================================
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The functionId that was set through `telemetry.functionId`
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const RESOURCE_NAME_ATTRIBUTE = 'resource.name';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The id of the model
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_MODEL_ID_ATTRIBUTE = 'ai.model.id';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The provider of the model
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_MODEL_PROVIDER_ATTRIBUTE = 'ai.model.provider';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The request headers that were passed in through `headers`
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_REQUEST_HEADERS_ATTRIBUTE = 'ai.request.headers';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The maximum number of retries that were set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_SETTINGS_MAX_RETRIES_ATTRIBUTE = 'ai.settings.maxRetries';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The functionId that was set through `telemetry.functionId`
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE = 'ai.telemetry.functionId';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The metadata that was passed in through `telemetry.metadata`
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_TELEMETRY_METADATA_ATTRIBUTE = 'ai.telemetry.metadata';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The number of completion tokens that were used
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE = 'ai.usage.completionTokens';
+
+/**
+ * Basic LLM span information
+ * Multiple spans
+ *
+ * The number of prompt tokens that were used
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
+ */
+export const AI_USAGE_PROMPT_TOKENS_ATTRIBUTE = 'ai.usage.promptTokens';
+
+// =============================================================================
+// CALL LLM SPAN INFORMATION
+// =============================================================================
+
+/**
+ * Call LLM span information
+ * Individual LLM call spans
+ *
+ * The model that was used to generate the response
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const AI_RESPONSE_MODEL_ATTRIBUTE = 'ai.response.model';
+
+/**
+ * Call LLM span information
+ * Individual LLM call spans
+ *
+ * The id of the response
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const AI_RESPONSE_ID_ATTRIBUTE = 'ai.response.id';
+
+/**
+ * Call LLM span information
+ * Individual LLM call spans
+ *
+ * The timestamp of the response
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const AI_RESPONSE_TIMESTAMP_ATTRIBUTE = 'ai.response.timestamp';
+
+// =============================================================================
+// SEMANTIC CONVENTIONS FOR GENAI OPERATIONS
+// =============================================================================
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The provider that was used
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_SYSTEM_ATTRIBUTE = 'gen_ai.system';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The model that was requested
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_MODEL_ATTRIBUTE = 'gen_ai.request.model';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The temperature that was set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE = 'gen_ai.request.temperature';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The maximum number of tokens that were set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE = 'gen_ai.request.max_tokens';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The frequency penalty that was set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE = 'gen_ai.request.frequency_penalty';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The presence penalty that was set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE = 'gen_ai.request.presence_penalty';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The topK parameter value that was set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_TOP_K_ATTRIBUTE = 'gen_ai.request.top_k';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The topP parameter value that was set
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_TOP_P_ATTRIBUTE = 'gen_ai.request.top_p';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The stop sequences
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_REQUEST_STOP_SEQUENCES_ATTRIBUTE = 'gen_ai.request.stop_sequences';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The finish reasons that were returned by the provider
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE = 'gen_ai.response.finish_reasons';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The model that was used to generate the response
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_RESPONSE_MODEL_ATTRIBUTE = 'gen_ai.response.model';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The id of the response
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_RESPONSE_ID_ATTRIBUTE = 'gen_ai.response.id';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The number of prompt tokens that were used
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE = 'gen_ai.usage.input_tokens';
+
+/**
+ * Semantic Conventions for GenAI operations
+ * Individual LLM call spans
+ *
+ * The number of completion tokens that were used
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#call-llm-span-information
+ */
+export const GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE = 'gen_ai.usage.output_tokens';
+
+// =============================================================================
+// BASIC EMBEDDING SPAN INFORMATION
+// =============================================================================
+
+/**
+ * Basic embedding span information
+ * Embedding spans
+ *
+ * The number of tokens that were used
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-embedding-span-information
+ */
+export const AI_USAGE_TOKENS_ATTRIBUTE = 'ai.usage.tokens';
+
+// =============================================================================
+// TOOL CALL SPANS
+// =============================================================================
+
+/**
+ * Tool call spans
+ * `ai.toolCall` span
+ *
+ * The name of the tool
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
+ */
+export const AI_TOOL_CALL_NAME_ATTRIBUTE = 'ai.toolCall.name';
+
+/**
+ * Tool call spans
+ * `ai.toolCall` span
+ *
+ * The id of the tool call
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
+ */
+export const AI_TOOL_CALL_ID_ATTRIBUTE = 'ai.toolCall.id';
+
+/**
+ * Tool call spans
+ * `ai.toolCall` span
+ *
+ * The parameters of the tool call
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
+ */
+export const AI_TOOL_CALL_ARGS_ATTRIBUTE = 'ai.toolCall.args';
+
+/**
+ * Tool call spans
+ * `ai.toolCall` span
+ *
+ * The result of the tool call
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
+ */
+export const AI_TOOL_CALL_RESULT_ATTRIBUTE = 'ai.toolCall.result';
+
+// =============================================================================
+// SPAN ATTRIBUTE OBJECTS
+// =============================================================================
+
+/**
+ * Attributes collected for `ai.generateText` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_GENERATE_TEXT_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_PROMPT: AI_PROMPT_ATTRIBUTE,
+ AI_RESPONSE_TEXT: AI_RESPONSE_TEXT_ATTRIBUTE,
+ AI_RESPONSE_TOOL_CALLS: AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
+ AI_RESPONSE_FINISH_REASON: AI_RESPONSE_FINISH_REASON_ATTRIBUTE,
+ AI_SETTINGS_MAX_STEPS: AI_SETTINGS_MAX_STEPS_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS: AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS: AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.generateText.doGenerate` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generatetext-function
+ */
+export const AI_GENERATE_TEXT_DO_GENERATE_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_PROMPT_FORMAT: AI_PROMPT_FORMAT_ATTRIBUTE,
+ AI_PROMPT_MESSAGES: AI_PROMPT_MESSAGES_ATTRIBUTE,
+ AI_PROMPT_TOOLS: AI_PROMPT_TOOLS_ATTRIBUTE,
+ AI_PROMPT_TOOL_CHOICE: AI_PROMPT_TOOL_CHOICE_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS: AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS: AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+ // Call LLM span information
+ AI_RESPONSE_MODEL: AI_RESPONSE_MODEL_ATTRIBUTE,
+ AI_RESPONSE_ID: AI_RESPONSE_ID_ATTRIBUTE,
+ AI_RESPONSE_TIMESTAMP: AI_RESPONSE_TIMESTAMP_ATTRIBUTE,
+ // Semantic Conventions for GenAI operations
+ GEN_AI_SYSTEM: GEN_AI_SYSTEM_ATTRIBUTE,
+ GEN_AI_REQUEST_MODEL: GEN_AI_REQUEST_MODEL_ATTRIBUTE,
+ GEN_AI_REQUEST_TEMPERATURE: GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE,
+ GEN_AI_REQUEST_MAX_TOKENS: GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
+ GEN_AI_REQUEST_FREQUENCY_PENALTY: GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE,
+ GEN_AI_REQUEST_PRESENCE_PENALTY: GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE,
+ GEN_AI_REQUEST_TOP_K: GEN_AI_REQUEST_TOP_K_ATTRIBUTE,
+ GEN_AI_REQUEST_TOP_P: GEN_AI_REQUEST_TOP_P_ATTRIBUTE,
+ GEN_AI_REQUEST_STOP_SEQUENCES: GEN_AI_REQUEST_STOP_SEQUENCES_ATTRIBUTE,
+ GEN_AI_RESPONSE_FINISH_REASONS: GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE,
+ GEN_AI_RESPONSE_MODEL: GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
+ GEN_AI_RESPONSE_ID: GEN_AI_RESPONSE_ID_ATTRIBUTE,
+ GEN_AI_USAGE_INPUT_TOKENS: GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_OUTPUT_TOKENS: GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.streamText` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamtext-function
+ */
+export const AI_STREAM_TEXT_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_PROMPT: AI_PROMPT_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS: AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS: AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.streamText.doStream` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamtext-function
+ */
+export const AI_STREAM_TEXT_DO_STREAM_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_RESPONSE_MS_TO_FIRST_CHUNK: AI_RESPONSE_MS_TO_FIRST_CHUNK_ATTRIBUTE,
+ AI_RESPONSE_MS_TO_FINISH: AI_RESPONSE_MS_TO_FINISH_ATTRIBUTE,
+ AI_RESPONSE_AVG_COMPLETION_TOKENS_PER_SECOND: AI_RESPONSE_AVG_COMPLETION_TOKENS_PER_SECOND_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS: AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS: AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+ // Call LLM span information
+ AI_RESPONSE_MODEL: AI_RESPONSE_MODEL_ATTRIBUTE,
+ AI_RESPONSE_ID: AI_RESPONSE_ID_ATTRIBUTE,
+ AI_RESPONSE_TIMESTAMP: AI_RESPONSE_TIMESTAMP_ATTRIBUTE,
+ // Semantic Conventions for GenAI operations
+ GEN_AI_SYSTEM: GEN_AI_SYSTEM_ATTRIBUTE,
+ GEN_AI_REQUEST_MODEL: GEN_AI_REQUEST_MODEL_ATTRIBUTE,
+ GEN_AI_REQUEST_TEMPERATURE: GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE,
+ GEN_AI_REQUEST_MAX_TOKENS: GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
+ GEN_AI_REQUEST_FREQUENCY_PENALTY: GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE,
+ GEN_AI_REQUEST_PRESENCE_PENALTY: GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE,
+ GEN_AI_REQUEST_TOP_K: GEN_AI_REQUEST_TOP_K_ATTRIBUTE,
+ GEN_AI_REQUEST_TOP_P: GEN_AI_REQUEST_TOP_P_ATTRIBUTE,
+ GEN_AI_REQUEST_STOP_SEQUENCES: GEN_AI_REQUEST_STOP_SEQUENCES_ATTRIBUTE,
+ GEN_AI_RESPONSE_FINISH_REASONS: GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE,
+ GEN_AI_RESPONSE_MODEL: GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
+ GEN_AI_RESPONSE_ID: GEN_AI_RESPONSE_ID_ATTRIBUTE,
+ GEN_AI_USAGE_INPUT_TOKENS: GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_OUTPUT_TOKENS: GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.generateObject` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#generateobject-function
+ */
+export const AI_GENERATE_OBJECT_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_SCHEMA: AI_SCHEMA_ATTRIBUTE,
+ AI_SCHEMA_NAME: AI_SCHEMA_NAME_ATTRIBUTE,
+ AI_SCHEMA_DESCRIPTION: AI_SCHEMA_DESCRIPTION_ATTRIBUTE,
+ AI_RESPONSE_OBJECT: AI_RESPONSE_OBJECT_ATTRIBUTE,
+ AI_SETTINGS_MODE: AI_SETTINGS_MODE_ATTRIBUTE,
+ AI_SETTINGS_OUTPUT: AI_SETTINGS_OUTPUT_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS: AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS: AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.streamObject` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#streamobject-function
+ */
+export const AI_STREAM_OBJECT_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_SCHEMA: AI_SCHEMA_ATTRIBUTE,
+ AI_SCHEMA_NAME: AI_SCHEMA_NAME_ATTRIBUTE,
+ AI_SCHEMA_DESCRIPTION: AI_SCHEMA_DESCRIPTION_ATTRIBUTE,
+ AI_RESPONSE_OBJECT: AI_RESPONSE_OBJECT_ATTRIBUTE,
+ AI_SETTINGS_MODE: AI_SETTINGS_MODE_ATTRIBUTE,
+ AI_SETTINGS_OUTPUT: AI_SETTINGS_OUTPUT_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS: AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS: AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.embed` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embed-function
+ */
+export const AI_EMBED_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_VALUE: AI_VALUE_ATTRIBUTE,
+ AI_EMBEDDING: AI_EMBEDDING_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ // Basic embedding span information
+ AI_USAGE_TOKENS: AI_USAGE_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.embed.doEmbed` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embed-function
+ */
+export const AI_EMBED_DO_EMBED_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_VALUES: AI_VALUES_ATTRIBUTE,
+ AI_EMBEDDINGS: AI_EMBEDDINGS_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ // Basic embedding span information
+ AI_USAGE_TOKENS: AI_USAGE_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.embedMany` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#embedmany-function
+ */
+export const AI_EMBED_MANY_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_VALUES: AI_VALUES_ATTRIBUTE,
+ AI_EMBEDDINGS: AI_EMBEDDINGS_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+ // Basic embedding span information
+ AI_USAGE_TOKENS: AI_USAGE_TOKENS_ATTRIBUTE,
+} as const;
+
+/**
+ * Attributes collected for `ai.toolCall` span
+ * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
+ */
+export const AI_TOOL_CALL_SPAN_ATTRIBUTES = {
+ OPERATION_NAME: OPERATION_NAME_ATTRIBUTE,
+ AI_OPERATION_ID: AI_OPERATION_ID_ATTRIBUTE,
+ AI_TOOL_CALL_NAME: AI_TOOL_CALL_NAME_ATTRIBUTE,
+ AI_TOOL_CALL_ID: AI_TOOL_CALL_ID_ATTRIBUTE,
+ AI_TOOL_CALL_ARGS: AI_TOOL_CALL_ARGS_ATTRIBUTE,
+ AI_TOOL_CALL_RESULT: AI_TOOL_CALL_RESULT_ATTRIBUTE,
+ // Basic LLM span information
+ RESOURCE_NAME: RESOURCE_NAME_ATTRIBUTE,
+ AI_MODEL_ID: AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER: AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_REQUEST_HEADERS: AI_REQUEST_HEADERS_ATTRIBUTE,
+ AI_SETTINGS_MAX_RETRIES: AI_SETTINGS_MAX_RETRIES_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
+} as const;
diff --git a/packages/node/src/integrations/tracing/vercelai/constants.ts b/packages/node/src/integrations/tracing/vercelai/constants.ts
new file mode 100644
index 000000000000..fd4473c4c084
--- /dev/null
+++ b/packages/node/src/integrations/tracing/vercelai/constants.ts
@@ -0,0 +1 @@
+export const INTEGRATION_NAME = 'VercelAI';
diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts
index f68b95f0f815..44bc2dca915f 100644
--- a/packages/node/src/integrations/tracing/vercelai/index.ts
+++ b/packages/node/src/integrations/tracing/vercelai/index.ts
@@ -3,17 +3,28 @@ import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core';
import { generateInstrumentOnce } from '../../../otel/instrument';
import { addOriginToSpan } from '../../../utils/addOriginToSpan';
+import {
+ AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_PROMPT_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+ GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
+ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
+} from './attributes';
+import { INTEGRATION_NAME } from './constants';
import { SentryVercelAiInstrumentation } from './instrumentation';
-
-const INTEGRATION_NAME = 'VercelAI';
+import type { VercelAiOptions } from './types';
export const instrumentVercelAi = generateInstrumentOnce(INTEGRATION_NAME, () => new SentryVercelAiInstrumentation({}));
-const _vercelAIIntegration = (() => {
+const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
let instrumentation: undefined | SentryVercelAiInstrumentation;
return {
name: INTEGRATION_NAME,
+ options,
setupOnce() {
instrumentation = instrumentVercelAi();
},
@@ -27,10 +38,10 @@ const _vercelAIIntegration = (() => {
}
// The id of the model
- const aiModelId = attributes['ai.model.id'];
+ const aiModelId = attributes[AI_MODEL_ID_ATTRIBUTE];
// the provider of the model
- const aiModelProvider = attributes['ai.model.provider'];
+ const aiModelProvider = attributes[AI_MODEL_PROVIDER_ATTRIBUTE];
// both of these must be defined for the integration to work
if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) {
@@ -114,11 +125,11 @@ const _vercelAIIntegration = (() => {
span.setAttribute('ai.pipeline.name', functionId);
}
- if (attributes['ai.prompt']) {
- span.setAttribute('ai.input_messages', attributes['ai.prompt']);
+ if (attributes[AI_PROMPT_ATTRIBUTE]) {
+ span.setAttribute('gen_ai.prompt', attributes[AI_PROMPT_ATTRIBUTE]);
}
- if (attributes['ai.model.id']) {
- span.setAttribute('ai.model_id', attributes['ai.model.id']);
+ if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) {
+ span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]);
}
span.setAttribute('ai.streaming', name.includes('stream'));
});
@@ -132,18 +143,22 @@ const _vercelAIIntegration = (() => {
continue;
}
- if (attributes['ai.usage.completionTokens'] != undefined) {
- attributes['ai.completion_tokens.used'] = attributes['ai.usage.completionTokens'];
+ if (attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE] != undefined) {
+ attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE];
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE];
}
- if (attributes['ai.usage.promptTokens'] != undefined) {
- attributes['ai.prompt_tokens.used'] = attributes['ai.usage.promptTokens'];
+ if (attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE] != undefined) {
+ attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE];
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE];
}
if (
- typeof attributes['ai.usage.completionTokens'] == 'number' &&
- typeof attributes['ai.usage.promptTokens'] == 'number'
+ typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' &&
+ typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number'
) {
- attributes['ai.total_tokens.used'] =
- attributes['ai.usage.completionTokens'] + attributes['ai.usage.promptTokens'];
+ attributes['gen_ai.usage.total_tokens'] =
+ attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] + attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE];
}
}
}
diff --git a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts
index e4f8a5ba25ae..4b823670793a 100644
--- a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts
+++ b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts
@@ -1,7 +1,8 @@
import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
-import { SDK_VERSION } from '@sentry/core';
-import type { TelemetrySettings } from './types';
+import { getCurrentScope, SDK_VERSION } from '@sentry/core';
+import { INTEGRATION_NAME } from './constants';
+import type { TelemetrySettings, VercelAiIntegration } from './types';
// List of patched methods
// From: https://sdk.vercel.ai/docs/ai-sdk-core/telemetry#collected-data
@@ -23,6 +24,47 @@ type MethodArgs = [MethodFirstArg, ...unknown[]];
type PatchedModuleExports = Record<(typeof INSTRUMENTED_METHODS)[number], (...args: MethodArgs) => unknown> &
Record;
+interface RecordingOptions {
+ recordInputs?: boolean;
+ recordOutputs?: boolean;
+}
+
+/**
+ * Determines whether to record inputs and outputs for Vercel AI telemetry based on the configuration hierarchy.
+ *
+ * The order of precedence is:
+ * 1. The vercel ai integration options
+ * 2. The experimental_telemetry options in the vercel ai method calls
+ * 3. When telemetry is explicitly enabled (isEnabled: true), default to recording
+ * 4. Otherwise, use the sendDefaultPii option from client options
+ */
+export function determineRecordingSettings(
+ integrationRecordingOptions: RecordingOptions | undefined,
+ methodTelemetryOptions: RecordingOptions,
+ telemetryExplicitlyEnabled: boolean | undefined,
+ defaultRecordingEnabled: boolean,
+): { recordInputs: boolean; recordOutputs: boolean } {
+ const recordInputs =
+ integrationRecordingOptions?.recordInputs !== undefined
+ ? integrationRecordingOptions.recordInputs
+ : methodTelemetryOptions.recordInputs !== undefined
+ ? methodTelemetryOptions.recordInputs
+ : telemetryExplicitlyEnabled === true
+ ? true // When telemetry is explicitly enabled, default to recording inputs
+ : defaultRecordingEnabled;
+
+ const recordOutputs =
+ integrationRecordingOptions?.recordOutputs !== undefined
+ ? integrationRecordingOptions.recordOutputs
+ : methodTelemetryOptions.recordOutputs !== undefined
+ ? methodTelemetryOptions.recordOutputs
+ : telemetryExplicitlyEnabled === true
+ ? true // When telemetry is explicitly enabled, default to recording inputs
+ : defaultRecordingEnabled;
+
+ return { recordInputs, recordOutputs };
+}
+
/**
* This detects is added by the Sentry Vercel AI Integration to detect if the integration should
* be enabled.
@@ -71,16 +113,24 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
const existingExperimentalTelemetry = args[0].experimental_telemetry || {};
const isEnabled = existingExperimentalTelemetry.isEnabled;
- // if `isEnabled` is not explicitly set to `true` or `false`, enable telemetry
- // but disable capturing inputs and outputs by default
- if (isEnabled === undefined) {
- args[0].experimental_telemetry = {
- isEnabled: true,
- recordInputs: false,
- recordOutputs: false,
- ...existingExperimentalTelemetry,
- };
- }
+ const client = getCurrentScope().getClient();
+ const integration = client?.getIntegrationByName(INTEGRATION_NAME);
+ const integrationOptions = integration?.options;
+ const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false;
+
+ const { recordInputs, recordOutputs } = determineRecordingSettings(
+ integrationOptions,
+ existingExperimentalTelemetry,
+ isEnabled,
+ shouldRecordInputsAndOutputs,
+ );
+
+ args[0].experimental_telemetry = {
+ ...existingExperimentalTelemetry,
+ isEnabled: isEnabled !== undefined ? isEnabled : true,
+ recordInputs,
+ recordOutputs,
+ };
// @ts-expect-error we know that the method exists
return originalMethod.apply(this, args);
diff --git a/packages/node/src/integrations/tracing/vercelai/types.ts b/packages/node/src/integrations/tracing/vercelai/types.ts
index 8773f84d52c6..50434b70604f 100644
--- a/packages/node/src/integrations/tracing/vercelai/types.ts
+++ b/packages/node/src/integrations/tracing/vercelai/types.ts
@@ -1,3 +1,5 @@
+import type { Integration } from '@sentry/core';
+
/**
* Telemetry configuration.
*/
@@ -42,3 +44,20 @@ export declare type AttributeValue =
| Array
| Array
| Array;
+
+export interface VercelAiOptions {
+ /**
+ * Enable or disable input recording. Enabled if `sendDefaultPii` is `true`
+ * or if you set `isEnabled` to `true` in your ai SDK method telemetry settings
+ */
+ recordInputs?: boolean;
+ /**
+ * Enable or disable output recording. Enabled if `sendDefaultPii` is `true`
+ * or if you set `isEnabled` to `true` in your ai SDK method telemetry settings
+ */
+ recordOutputs?: boolean;
+}
+
+export interface VercelAiIntegration extends Integration {
+ options: VercelAiOptions;
+}
diff --git a/packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts b/packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts
new file mode 100644
index 000000000000..9a9d8cc50f0a
--- /dev/null
+++ b/packages/node/test/integrations/tracing/vercelai/instrumentation.test.ts
@@ -0,0 +1,214 @@
+import { describe, expect, test } from 'vitest';
+import { determineRecordingSettings } from '../../../../src/integrations/tracing/vercelai/instrumentation';
+
+describe('determineRecordingSettings', () => {
+ test('should use integration recording options when provided (recordInputs: true, recordOutputs: false)', () => {
+ const result = determineRecordingSettings(
+ { recordInputs: true, recordOutputs: false }, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ undefined, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: false,
+ });
+ });
+
+ test('should use integration recording options when provided (recordInputs: false, recordOutputs: true)', () => {
+ const result = determineRecordingSettings(
+ { recordInputs: false, recordOutputs: true }, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ true, // telemetryExplicitlyEnabled
+ true, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: false,
+ recordOutputs: true,
+ });
+ });
+
+ test('should fall back to method telemetry options when integration options not provided', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ { recordInputs: true, recordOutputs: false }, // methodTelemetryOptions
+ undefined, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: false,
+ });
+ });
+
+ test('should prefer integration recording options over method telemetry options', () => {
+ const result = determineRecordingSettings(
+ { recordInputs: false, recordOutputs: false }, // integrationRecordingOptions
+ { recordInputs: true, recordOutputs: true }, // methodTelemetryOptions
+ undefined, // telemetryExplicitlyEnabled
+ true, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: false,
+ recordOutputs: false,
+ });
+ });
+
+ test('should default to recording when telemetry is explicitly enabled', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ true, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: true,
+ });
+ });
+
+ test('should use default recording setting when telemetry is explicitly disabled', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ false, // telemetryExplicitlyEnabled
+ true, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: true,
+ });
+ });
+
+ test('should use default recording setting when telemetry enablement is undefined', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ undefined, // telemetryExplicitlyEnabled
+ true, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: true,
+ });
+ });
+
+ test('should not record when default recording is disabled and no explicit configuration', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ undefined, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: false,
+ recordOutputs: false,
+ });
+ });
+
+ test('should handle partial integration recording options (only recordInputs)', () => {
+ const result = determineRecordingSettings(
+ { recordInputs: true }, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ false, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: false, // falls back to defaultRecordingEnabled
+ });
+ });
+
+ test('should handle partial integration recording options (only recordOutputs)', () => {
+ const result = determineRecordingSettings(
+ { recordOutputs: true }, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ false, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: false, // falls back to defaultRecordingEnabled
+ recordOutputs: true,
+ });
+ });
+
+ test('should handle partial method telemetry options (only recordInputs)', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ { recordInputs: true }, // methodTelemetryOptions
+ false, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: false, // falls back to defaultRecordingEnabled
+ });
+ });
+
+ test('should handle partial method telemetry options (only recordOutputs)', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ { recordOutputs: true }, // methodTelemetryOptions
+ false, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: false, // falls back to defaultRecordingEnabled
+ recordOutputs: true,
+ });
+ });
+
+ test('should prefer integration recording options over method telemetry for partial configs', () => {
+ const result = determineRecordingSettings(
+ { recordInputs: false }, // integrationRecordingOptions
+ { recordInputs: true, recordOutputs: true }, // methodTelemetryOptions
+ false, // telemetryExplicitlyEnabled
+ true, // defaultRecordingEnabled
+ );
+
+ expect(result).toEqual({
+ recordInputs: false, // from integration recording options
+ recordOutputs: true, // from method telemetry options
+ });
+ });
+
+ test('complex scenario: sendDefaultPii enabled, telemetry enablement undefined, mixed options', () => {
+ const result = determineRecordingSettings(
+ { recordOutputs: false }, // integrationRecordingOptions
+ { recordInputs: false }, // methodTelemetryOptions
+ undefined, // telemetryExplicitlyEnabled
+ true, // defaultRecordingEnabled (sendDefaultPii: true)
+ );
+
+ expect(result).toEqual({
+ recordInputs: false, // from method telemetry options
+ recordOutputs: false, // from integration recording options
+ });
+ });
+
+ test('complex scenario: explicit telemetry enabled overrides sendDefaultPii disabled', () => {
+ const result = determineRecordingSettings(
+ {}, // integrationRecordingOptions
+ {}, // methodTelemetryOptions
+ true, // telemetryExplicitlyEnabled
+ false, // defaultRecordingEnabled (sendDefaultPii: false)
+ );
+
+ expect(result).toEqual({
+ recordInputs: true,
+ recordOutputs: true,
+ });
+ });
+});
diff --git a/packages/react-router/src/server/index.ts b/packages/react-router/src/server/index.ts
index 67436582aedd..b42b769a78e6 100644
--- a/packages/react-router/src/server/index.ts
+++ b/packages/react-router/src/server/index.ts
@@ -4,3 +4,5 @@ export { init } from './sdk';
// eslint-disable-next-line deprecation/deprecation
export { wrapSentryHandleRequest, sentryHandleRequest, getMetaTagTransformer } from './wrapSentryHandleRequest';
export { createSentryHandleRequest, type SentryHandleRequestOptions } from './createSentryHandleRequest';
+export { wrapServerAction } from './wrapServerAction';
+export { wrapServerLoader } from './wrapServerLoader';
diff --git a/packages/react-router/src/server/sdk.ts b/packages/react-router/src/server/sdk.ts
index 55eaf6962a28..07ea80e867ea 100644
--- a/packages/react-router/src/server/sdk.ts
+++ b/packages/react-router/src/server/sdk.ts
@@ -2,7 +2,7 @@ import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import type { EventProcessor, Integration } from '@sentry/core';
import { applySdkMetadata, getGlobalScope, logger, setTag } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
-import { getDefaultIntegrations as getNodeDefaultIntegrations, init as initNodeSdk } from '@sentry/node';
+import { getDefaultIntegrations as getNodeDefaultIntegrations, init as initNodeSdk, NODE_VERSION } from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE } from './instrumentation/util';
import { lowQualityTransactionsFilterIntegration } from './integration/lowQualityTransactionsFilterIntegration';
@@ -13,11 +13,16 @@ import { reactRouterServerIntegration } from './integration/reactRouterServer';
* @param options The options for the SDK.
*/
export function getDefaultReactRouterServerIntegrations(options: NodeOptions): Integration[] {
- return [
- ...getNodeDefaultIntegrations(options),
- lowQualityTransactionsFilterIntegration(options),
- reactRouterServerIntegration(),
- ];
+ const integrations = [...getNodeDefaultIntegrations(options), lowQualityTransactionsFilterIntegration(options)];
+
+ if (
+ (NODE_VERSION.major === 20 && NODE_VERSION.minor < 19) || // https://nodejs.org/en/blog/release/v20.19.0
+ (NODE_VERSION.major === 22 && NODE_VERSION.minor < 12) // https://nodejs.org/en/blog/release/v22.12.0
+ ) {
+ integrations.push(reactRouterServerIntegration());
+ }
+
+ return integrations;
}
/**
diff --git a/packages/react-router/src/server/wrapServerAction.ts b/packages/react-router/src/server/wrapServerAction.ts
new file mode 100644
index 000000000000..9da0e8d351f8
--- /dev/null
+++ b/packages/react-router/src/server/wrapServerAction.ts
@@ -0,0 +1,70 @@
+import type { SpanAttributes } from '@sentry/core';
+import {
+ getActiveSpan,
+ getRootSpan,
+ parseStringToURLObject,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ spanToJSON,
+ startSpan,
+} from '@sentry/core';
+import type { ActionFunctionArgs } from 'react-router';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE } from './instrumentation/util';
+
+type SpanOptions = {
+ name?: string;
+ attributes?: SpanAttributes;
+};
+
+/**
+ * Wraps a React Router server action function with Sentry performance monitoring.
+ * @param options - Optional span configuration options including name, operation, description and attributes
+ * @param actionFn - The server action function to wrap
+ *
+ * @example
+ * ```ts
+ * // Wrap an action function with custom span options
+ * export const action = wrapServerAction(
+ * {
+ * name: 'Submit Form Data',
+ * description: 'Processes form submission data',
+ * },
+ * async ({ request }) => {
+ * // ... your action logic
+ * }
+ * );
+ * ```
+ */
+export function wrapServerAction(options: SpanOptions = {}, actionFn: (args: ActionFunctionArgs) => Promise) {
+ return async function (args: ActionFunctionArgs) {
+ const name = options.name || 'Executing Server Action';
+ const active = getActiveSpan();
+ if (active) {
+ const root = getRootSpan(active);
+ // coming from auto.http.otel.http
+ if (spanToJSON(root).description === 'POST') {
+ const url = parseStringToURLObject(args.request.url);
+ if (url?.pathname) {
+ root.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE]: `${args.request.method} ${url.pathname}`,
+ });
+ }
+ }
+ }
+
+ return startSpan(
+ {
+ name,
+ ...options,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react-router.action',
+ ...options.attributes,
+ },
+ },
+ () => actionFn(args),
+ );
+ };
+}
diff --git a/packages/react-router/src/server/wrapServerLoader.ts b/packages/react-router/src/server/wrapServerLoader.ts
new file mode 100644
index 000000000000..dda64a1a9204
--- /dev/null
+++ b/packages/react-router/src/server/wrapServerLoader.ts
@@ -0,0 +1,70 @@
+import type { SpanAttributes } from '@sentry/core';
+import {
+ getActiveSpan,
+ getRootSpan,
+ parseStringToURLObject,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ spanToJSON,
+ startSpan,
+} from '@sentry/core';
+import type { LoaderFunctionArgs } from 'react-router';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE } from './instrumentation/util';
+
+type SpanOptions = {
+ name?: string;
+ attributes?: SpanAttributes;
+};
+
+/**
+ * Wraps a React Router server loader function with Sentry performance monitoring.
+ * @param options - Optional span configuration options including name, operation, description and attributes
+ * @param loaderFn - The server loader function to wrap
+ *
+ * @example
+ * ```ts
+ * // Wrap a loader function with custom span options
+ * export const loader = wrapServerLoader(
+ * {
+ * name: 'Load Some Data',
+ * description: 'Loads some data from the db',
+ * },
+ * async ({ params }) => {
+ * // ... your loader logic
+ * }
+ * );
+ * ```
+ */
+export function wrapServerLoader(options: SpanOptions = {}, loaderFn: (args: LoaderFunctionArgs) => Promise) {
+ return async function (args: LoaderFunctionArgs) {
+ const name = options.name || 'Executing Server Loader';
+ const active = getActiveSpan();
+ if (active) {
+ const root = getRootSpan(active);
+ // coming from auto.http.otel.http
+ if (spanToJSON(root).description === 'GET') {
+ const url = parseStringToURLObject(args.request.url);
+
+ if (url?.pathname) {
+ root.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE]: `${args.request.method} ${url.pathname}`,
+ });
+ }
+ }
+ }
+ return startSpan(
+ {
+ name,
+ ...options,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react-router.loader',
+ ...options.attributes,
+ },
+ },
+ () => loaderFn(args),
+ );
+ };
+}
diff --git a/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts b/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts
index 41580f4a7c7f..80e540c9760a 100644
--- a/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts
+++ b/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts
@@ -14,6 +14,7 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil
org,
project,
telemetry,
+ reactComponentAnnotation,
release,
} = options;
@@ -30,6 +31,11 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil
},
...unstable_sentryVitePluginOptions?._metaOptions,
},
+ reactComponentAnnotation: {
+ enabled: reactComponentAnnotation?.enabled ?? undefined,
+ ignoredComponents: reactComponentAnnotation?.ignoredComponents ?? undefined,
+ ...unstable_sentryVitePluginOptions?.reactComponentAnnotation,
+ },
release: {
...unstable_sentryVitePluginOptions?.release,
...release,
@@ -45,7 +51,13 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil
// only use a subset of the plugins as all upload and file deletion tasks will be handled in the buildEnd hook
return [
...sentryVitePlugins.filter(plugin => {
- return ['sentry-telemetry-plugin', 'sentry-vite-release-injection-plugin'].includes(plugin.name);
+ return [
+ 'sentry-telemetry-plugin',
+ 'sentry-vite-release-injection-plugin',
+ ...(reactComponentAnnotation?.enabled || unstable_sentryVitePluginOptions?.reactComponentAnnotation?.enabled
+ ? ['sentry-vite-component-name-annotate-plugin']
+ : []),
+ ].includes(plugin.name);
}),
];
}
diff --git a/packages/react-router/src/vite/types.ts b/packages/react-router/src/vite/types.ts
index de8175b0141c..fb488d2ca8bc 100644
--- a/packages/react-router/src/vite/types.ts
+++ b/packages/react-router/src/vite/types.ts
@@ -125,6 +125,25 @@ export type SentryReactRouterBuildOptions = {
*/
debug?: boolean;
+ /**
+ * Options related to react component name annotations.
+ * Disabled by default, unless a value is set for this option.
+ * When enabled, your app's DOM will automatically be annotated during build-time with their respective component names.
+ * This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in breadcrumbs and performance monitoring.
+ * Please note that this feature is not currently supported by the esbuild bundler plugins, and will only annotate React components
+ */
+ reactComponentAnnotation?: {
+ /**
+ * Whether the component name annotate plugin should be enabled or not.
+ */
+ enabled?: boolean;
+
+ /**
+ * A list of strings representing the names of components to ignore. The plugin will not apply `data-sentry` annotations on the DOM element for these components.
+ */
+ ignoredComponents?: string[];
+ };
+
/**
* Options for the Sentry Vite plugin to customize the source maps upload process.
*
diff --git a/packages/react-router/test/server/sdk.test.ts b/packages/react-router/test/server/sdk.test.ts
index fdb894299760..861144e3f62b 100644
--- a/packages/react-router/test/server/sdk.test.ts
+++ b/packages/react-router/test/server/sdk.test.ts
@@ -71,5 +71,77 @@ describe('React Router server SDK', () => {
expect(filterIntegration).toBeDefined();
});
+
+ it('adds reactRouterServer integration for Node.js 20.18', () => {
+ vi.spyOn(SentryNode, 'NODE_VERSION', 'get').mockReturnValue({ major: 20, minor: 18, patch: 0 });
+
+ reactRouterInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
+
+ expect(nodeInit).toHaveBeenCalledTimes(1);
+ const initOptions = nodeInit.mock.calls[0]?.[0];
+ const defaultIntegrations = initOptions?.defaultIntegrations as Integration[];
+
+ const reactRouterServerIntegration = defaultIntegrations.find(
+ integration => integration.name === 'ReactRouterServer',
+ );
+
+ expect(reactRouterServerIntegration).toBeDefined();
+ });
+
+ it('adds reactRouterServer integration for Node.js 22.11', () => {
+ vi.spyOn(SentryNode, 'NODE_VERSION', 'get').mockReturnValue({ major: 22, minor: 11, patch: 0 });
+
+ reactRouterInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
+
+ expect(nodeInit).toHaveBeenCalledTimes(1);
+ const initOptions = nodeInit.mock.calls[0]?.[0];
+ const defaultIntegrations = initOptions?.defaultIntegrations as Integration[];
+
+ const reactRouterServerIntegration = defaultIntegrations.find(
+ integration => integration.name === 'ReactRouterServer',
+ );
+
+ expect(reactRouterServerIntegration).toBeDefined();
+ });
+
+ it('does not add reactRouterServer integration for Node.js 20.19', () => {
+ vi.spyOn(SentryNode, 'NODE_VERSION', 'get').mockReturnValue({ major: 20, minor: 19, patch: 0 });
+
+ reactRouterInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
+
+ expect(nodeInit).toHaveBeenCalledTimes(1);
+ const initOptions = nodeInit.mock.calls[0]?.[0];
+ const defaultIntegrations = initOptions?.defaultIntegrations as Integration[];
+
+ const reactRouterServerIntegration = defaultIntegrations.find(
+ integration => integration.name === 'ReactRouterServer',
+ );
+
+ expect(reactRouterServerIntegration).toBeUndefined();
+ });
+
+ it('does not add reactRouterServer integration for Node.js 22.12', () => {
+ vi.spyOn(SentryNode, 'NODE_VERSION', 'get').mockReturnValue({ major: 22, minor: 12, patch: 0 });
+
+ reactRouterInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
+
+ expect(nodeInit).toHaveBeenCalledTimes(1);
+ const initOptions = nodeInit.mock.calls[0]?.[0];
+ const defaultIntegrations = initOptions?.defaultIntegrations as Integration[];
+
+ const reactRouterServerIntegration = defaultIntegrations.find(
+ integration => integration.name === 'ReactRouterServer',
+ );
+
+ expect(reactRouterServerIntegration).toBeUndefined();
+ });
});
});
diff --git a/packages/react-router/test/server/wrapServerAction.test.ts b/packages/react-router/test/server/wrapServerAction.test.ts
new file mode 100644
index 000000000000..931e4c72b446
--- /dev/null
+++ b/packages/react-router/test/server/wrapServerAction.test.ts
@@ -0,0 +1,60 @@
+import * as core from '@sentry/core';
+import type { ActionFunctionArgs } from 'react-router';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { wrapServerAction } from '../../src/server/wrapServerAction';
+
+describe('wrapServerAction', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should wrap an action function with default options', async () => {
+ const mockActionFn = vi.fn().mockResolvedValue('result');
+ const mockArgs = { request: new Request('http://test.com') } as ActionFunctionArgs;
+
+ const spy = vi.spyOn(core, 'startSpan');
+ const wrappedAction = wrapServerAction({}, mockActionFn);
+ await wrappedAction(mockArgs);
+
+ expect(spy).toHaveBeenCalledWith(
+ {
+ name: 'Executing Server Action',
+ attributes: {
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react-router.action',
+ },
+ },
+ expect.any(Function),
+ );
+ expect(mockActionFn).toHaveBeenCalledWith(mockArgs);
+ });
+
+ it('should wrap an action function with custom options', async () => {
+ const customOptions = {
+ name: 'Custom Action',
+ attributes: {
+ 'sentry.custom': 'value',
+ },
+ };
+
+ const mockActionFn = vi.fn().mockResolvedValue('result');
+ const mockArgs = { request: new Request('http://test.com') } as ActionFunctionArgs;
+
+ const spy = vi.spyOn(core, 'startSpan');
+ const wrappedAction = wrapServerAction(customOptions, mockActionFn);
+ await wrappedAction(mockArgs);
+
+ expect(spy).toHaveBeenCalledWith(
+ {
+ name: 'Custom Action',
+ attributes: {
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react-router.action',
+ 'sentry.custom': 'value',
+ },
+ },
+ expect.any(Function),
+ );
+ expect(mockActionFn).toHaveBeenCalledWith(mockArgs);
+ });
+});
diff --git a/packages/react-router/test/server/wrapServerLoader.test.ts b/packages/react-router/test/server/wrapServerLoader.test.ts
new file mode 100644
index 000000000000..53fce752286b
--- /dev/null
+++ b/packages/react-router/test/server/wrapServerLoader.test.ts
@@ -0,0 +1,60 @@
+import * as core from '@sentry/core';
+import type { LoaderFunctionArgs } from 'react-router';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { wrapServerLoader } from '../../src/server/wrapServerLoader';
+
+describe('wrapServerLoader', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should wrap a loader function with default options', async () => {
+ const mockLoaderFn = vi.fn().mockResolvedValue('result');
+ const mockArgs = { request: new Request('http://test.com') } as LoaderFunctionArgs;
+
+ const spy = vi.spyOn(core, 'startSpan');
+ const wrappedLoader = wrapServerLoader({}, mockLoaderFn);
+ await wrappedLoader(mockArgs);
+
+ expect(spy).toHaveBeenCalledWith(
+ {
+ name: 'Executing Server Loader',
+ attributes: {
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react-router.loader',
+ },
+ },
+ expect.any(Function),
+ );
+ expect(mockLoaderFn).toHaveBeenCalledWith(mockArgs);
+ });
+
+ it('should wrap a loader function with custom options', async () => {
+ const customOptions = {
+ name: 'Custom Loader',
+ attributes: {
+ 'sentry.custom': 'value',
+ },
+ };
+
+ const mockLoaderFn = vi.fn().mockResolvedValue('result');
+ const mockArgs = { request: new Request('http://test.com') } as LoaderFunctionArgs;
+
+ const spy = vi.spyOn(core, 'startSpan');
+ const wrappedLoader = wrapServerLoader(customOptions, mockLoaderFn);
+ await wrappedLoader(mockArgs);
+
+ expect(spy).toHaveBeenCalledWith(
+ {
+ name: 'Custom Loader',
+ attributes: {
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
+ [core.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react-router.loader',
+ 'sentry.custom': 'value',
+ },
+ },
+ expect.any(Function),
+ );
+ expect(mockLoaderFn).toHaveBeenCalledWith(mockArgs);
+ });
+});
diff --git a/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts b/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts
index 04576076a561..b4db6d85d028 100644
--- a/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts
+++ b/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts
@@ -8,6 +8,7 @@ vi.mock('@sentry/vite-plugin', () => ({
.mockReturnValue([
{ name: 'sentry-telemetry-plugin' },
{ name: 'sentry-vite-release-injection-plugin' },
+ { name: 'sentry-vite-component-name-annotate-plugin' },
{ name: 'other-plugin' },
]),
}));
@@ -60,4 +61,24 @@ describe('makeCustomSentryVitePlugins', () => {
expect(plugins?.[0]?.name).toBe('sentry-telemetry-plugin');
expect(plugins?.[1]?.name).toBe('sentry-vite-release-injection-plugin');
});
+
+ it('should include component annotation plugin when reactComponentAnnotation.enabled is true', async () => {
+ const plugins = await makeCustomSentryVitePlugins({ reactComponentAnnotation: { enabled: true } });
+
+ expect(plugins).toHaveLength(3);
+ expect(plugins?.[0]?.name).toBe('sentry-telemetry-plugin');
+ expect(plugins?.[1]?.name).toBe('sentry-vite-release-injection-plugin');
+ expect(plugins?.[2]?.name).toBe('sentry-vite-component-name-annotate-plugin');
+ });
+
+ it('should include component annotation plugin when unstable_sentryVitePluginOptions.reactComponentAnnotation.enabled is true', async () => {
+ const plugins = await makeCustomSentryVitePlugins({
+ unstable_sentryVitePluginOptions: { reactComponentAnnotation: { enabled: true } },
+ });
+
+ expect(plugins).toHaveLength(3);
+ expect(plugins?.[0]?.name).toBe('sentry-telemetry-plugin');
+ expect(plugins?.[1]?.name).toBe('sentry-vite-release-injection-plugin');
+ expect(plugins?.[2]?.name).toBe('sentry-vite-component-name-annotate-plugin');
+ });
});
diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts
index e4e2fdb70f4e..5aadfdd876be 100644
--- a/packages/vue/src/tracing.ts
+++ b/packages/vue/src/tracing.ts
@@ -12,11 +12,11 @@ type Mixins = Parameters[0];
interface VueSentry extends ViewModel {
readonly $root: VueSentry;
- $_sentrySpans?: {
+ $_sentryComponentSpans?: {
[key: string]: Span | undefined;
};
- $_sentryRootSpan?: Span;
- $_sentryRootSpanTimer?: ReturnType;
+ $_sentryRootComponentSpan?: Span;
+ $_sentryRootComponentSpanTimer?: ReturnType;
}
// Mappings from operation to corresponding lifecycle hook.
@@ -31,16 +31,16 @@ const HOOKS: { [key in Operation]: Hook[] } = {
update: ['beforeUpdate', 'updated'],
};
-/** Finish top-level span and activity with a debounce configured using `timeout` option */
-function finishRootSpan(vm: VueSentry, timestamp: number, timeout: number): void {
- if (vm.$_sentryRootSpanTimer) {
- clearTimeout(vm.$_sentryRootSpanTimer);
+/** Finish top-level component span and activity with a debounce configured using `timeout` option */
+function finishRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void {
+ if (vm.$_sentryRootComponentSpanTimer) {
+ clearTimeout(vm.$_sentryRootComponentSpanTimer);
}
- vm.$_sentryRootSpanTimer = setTimeout(() => {
- if (vm.$root?.$_sentryRootSpan) {
- vm.$root.$_sentryRootSpan.end(timestamp);
- vm.$root.$_sentryRootSpan = undefined;
+ vm.$_sentryRootComponentSpanTimer = setTimeout(() => {
+ if (vm.$root?.$_sentryRootComponentSpan) {
+ vm.$root.$_sentryRootComponentSpan.end(timestamp);
+ vm.$root.$_sentryRootComponentSpan = undefined;
}
}, timeout);
}
@@ -77,11 +77,12 @@ export const createTracingMixins = (options: Partial = {}): Mixi
for (const internalHook of internalHooks) {
mixins[internalHook] = function (this: VueSentry) {
- const isRoot = this.$root === this;
+ const isRootComponent = this.$root === this;
- if (isRoot) {
- this.$_sentryRootSpan =
- this.$_sentryRootSpan ||
+ // 1. Root Component span creation
+ if (isRootComponent) {
+ this.$_sentryRootComponentSpan =
+ this.$_sentryRootComponentSpan ||
startInactiveSpan({
name: 'Application Render',
op: `${VUE_OP}.render`,
@@ -92,35 +93,39 @@ export const createTracingMixins = (options: Partial = {}): Mixi
});
}
- // Skip components that we don't want to track to minimize the noise and give a more granular control to the user
- const name = formatComponentName(this, false);
+ // 2. Component tracking filter
+ const componentName = formatComponentName(this, false);
- const shouldTrack = Array.isArray(options.trackComponents)
- ? findTrackComponent(options.trackComponents, name)
- : options.trackComponents;
+ const shouldTrack =
+ isRootComponent || // We always want to track the root component
+ (Array.isArray(options.trackComponents)
+ ? findTrackComponent(options.trackComponents, componentName)
+ : options.trackComponents);
- // We always want to track root component
- if (!isRoot && !shouldTrack) {
+ if (!shouldTrack) {
return;
}
- this.$_sentrySpans = this.$_sentrySpans || {};
+ this.$_sentryComponentSpans = this.$_sentryComponentSpans || {};
- // Start a new span if current hook is a 'before' hook.
- // Otherwise, retrieve the current span and finish it.
- if (internalHook == internalHooks[0]) {
- const activeSpan = this.$root?.$_sentryRootSpan || getActiveSpan();
+ // 3. Span lifecycle management based on the hook type
+ const isBeforeHook = internalHook === internalHooks[0];
+ const activeSpan = this.$root?.$_sentryRootComponentSpan || getActiveSpan();
+
+ if (isBeforeHook) {
+ // Starting a new span in the "before" hook
if (activeSpan) {
- // Cancel old span for this hook operation in case it didn't get cleaned up. We're not actually sure if it
- // will ever be the case that cleanup hooks re not called, but we had users report that spans didn't get
- // finished so we finish the span before starting a new one, just to be sure.
- const oldSpan = this.$_sentrySpans[operation];
+ // Cancel any existing span for this operation (safety measure)
+ // We're actually not sure if it will ever be the case that cleanup hooks were not called.
+ // However, we had users report that spans didn't get finished, so we finished the span before
+ // starting a new one, just to be sure.
+ const oldSpan = this.$_sentryComponentSpans[operation];
if (oldSpan) {
oldSpan.end();
}
- this.$_sentrySpans[operation] = startInactiveSpan({
- name: `Vue ${name}`,
+ this.$_sentryComponentSpans[operation] = startInactiveSpan({
+ name: `Vue ${componentName}`,
op: `${VUE_OP}.${operation}`,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue',
@@ -131,13 +136,14 @@ export const createTracingMixins = (options: Partial = {}): Mixi
}
} else {
// The span should already be added via the first handler call (in the 'before' hook)
- const span = this.$_sentrySpans[operation];
+ const span = this.$_sentryComponentSpans[operation];
// The before hook did not start the tracking span, so the span was not added.
// This is probably because it happened before there is an active transaction
- if (!span) return;
+ if (!span) return; // Skip if no span was created in the "before" hook
span.end();
- finishRootSpan(this, timestampInSeconds(), options.timeout || 2000);
+ // For any "after" hook, also schedule the root component span to finish
+ finishRootComponentSpan(this, timestampInSeconds(), options.timeout || 2000);
}
};
}
diff --git a/packages/vue/test/tracing/tracingMixin.test.ts b/packages/vue/test/tracing/tracingMixin.test.ts
new file mode 100644
index 000000000000..d67690271ed2
--- /dev/null
+++ b/packages/vue/test/tracing/tracingMixin.test.ts
@@ -0,0 +1,240 @@
+import { getActiveSpan, startInactiveSpan } from '@sentry/browser';
+import type { Mock } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { DEFAULT_HOOKS } from '../../src/constants';
+import { createTracingMixins } from '../../src/tracing';
+
+vi.mock('@sentry/browser', () => {
+ return {
+ getActiveSpan: vi.fn(),
+ startInactiveSpan: vi.fn().mockImplementation(({ name, op }) => {
+ return {
+ end: vi.fn(),
+ startChild: vi.fn(),
+ name,
+ op,
+ };
+ }),
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin',
+ };
+});
+
+vi.mock('../../src/vendor/components', () => {
+ return {
+ formatComponentName: vi.fn().mockImplementation(vm => {
+ return vm.componentName || 'TestComponent';
+ }),
+ };
+});
+
+const mockSpanFactory = (): { name?: string; op?: string; end: Mock; startChild: Mock } => ({
+ name: undefined,
+ op: undefined,
+ end: vi.fn(),
+ startChild: vi.fn(),
+});
+
+vi.useFakeTimers();
+
+describe('Vue Tracing Mixins', () => {
+ let mockVueInstance: any;
+ let mockRootInstance: any;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ mockRootInstance = {
+ $root: null,
+ componentName: 'RootComponent',
+ $_sentryComponentSpans: {},
+ };
+ mockRootInstance.$root = mockRootInstance; // Self-reference for root
+
+ mockVueInstance = {
+ $root: mockRootInstance,
+ componentName: 'TestComponent',
+ $_sentryComponentSpans: {},
+ };
+
+ (getActiveSpan as any).mockReturnValue({ id: 'parent-span' });
+ (startInactiveSpan as any).mockImplementation(({ name, op }: { name: string; op: string }) => {
+ const newSpan = mockSpanFactory();
+ newSpan.name = name;
+ newSpan.op = op;
+ return newSpan;
+ });
+ });
+
+ afterEach(() => {
+ vi.clearAllTimers();
+ });
+
+ describe('Mixin Creation', () => {
+ it('should create mixins for default hooks', () => {
+ const mixins = createTracingMixins();
+
+ DEFAULT_HOOKS.forEach(hook => {
+ const hookPairs = {
+ mount: ['beforeMount', 'mounted'],
+ update: ['beforeUpdate', 'updated'],
+ destroy: ['beforeDestroy', 'destroyed'],
+ unmount: ['beforeUnmount', 'unmounted'],
+ create: ['beforeCreate', 'created'],
+ activate: ['activated', 'deactivated'],
+ };
+
+ if (hook in hookPairs) {
+ hookPairs[hook as keyof typeof hookPairs].forEach(lifecycleHook => {
+ expect(mixins).toHaveProperty(lifecycleHook);
+ // @ts-expect-error we check the type here
+ expect(typeof mixins[lifecycleHook]).toBe('function');
+ });
+ }
+ });
+ });
+
+ it('should always include the activate and mount hooks', () => {
+ const mixins = createTracingMixins({ hooks: undefined });
+
+ expect(Object.keys(mixins)).toEqual(['activated', 'deactivated', 'beforeMount', 'mounted']);
+ });
+
+ it('should create mixins for custom hooks', () => {
+ const mixins = createTracingMixins({ hooks: ['update'] });
+
+ expect(Object.keys(mixins)).toEqual([
+ 'beforeUpdate',
+ 'updated',
+ 'activated',
+ 'deactivated',
+ 'beforeMount',
+ 'mounted',
+ ]);
+ });
+ });
+
+ describe('Root Component Behavior', () => {
+ it('should always create a root component span for the Vue root component regardless of tracking options', () => {
+ const mixins = createTracingMixins({ trackComponents: false });
+
+ mixins.beforeMount.call(mockRootInstance);
+
+ expect(startInactiveSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'Application Render',
+ op: 'ui.vue.render',
+ }),
+ );
+ });
+
+ it('should finish root component span on timer after component spans end', () => {
+ // todo/fixme: This root component span is only finished if trackComponents is true --> it should probably be always finished
+ const mixins = createTracingMixins({ trackComponents: true, timeout: 1000 });
+ const rootMockSpan = mockSpanFactory();
+ mockRootInstance.$_sentryRootComponentSpan = rootMockSpan;
+
+ // Create and finish a component span
+ mixins.beforeMount.call(mockVueInstance);
+ mixins.mounted.call(mockVueInstance);
+
+ // Root component span should not end immediately
+ expect(rootMockSpan.end).not.toHaveBeenCalled();
+
+ // After timeout, root component span should end
+ vi.advanceTimersByTime(1001);
+ expect(rootMockSpan.end).toHaveBeenCalled();
+ });
+ });
+
+ describe('Component Span Lifecycle', () => {
+ it('should create and end spans correctly through lifecycle hooks', () => {
+ const mixins = createTracingMixins({ trackComponents: true });
+
+ // 1. Create span in "before" hook
+ mixins.beforeMount.call(mockVueInstance);
+
+ // Verify span was created with correct details
+ expect(startInactiveSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'Vue TestComponent',
+ op: 'ui.vue.mount',
+ }),
+ );
+ expect(mockVueInstance.$_sentryComponentSpans.mount).toBeDefined();
+
+ // 2. Get the span for verification
+ const componentSpan = mockVueInstance.$_sentryComponentSpans.mount;
+
+ // 3. End span in "after" hook
+ mixins.mounted.call(mockVueInstance);
+ expect(componentSpan.end).toHaveBeenCalled();
+ });
+
+ it('should clean up existing spans when creating new ones', () => {
+ const mixins = createTracingMixins({ trackComponents: true });
+
+ // Create an existing span first
+ const oldSpan = mockSpanFactory();
+ mockVueInstance.$_sentryComponentSpans.mount = oldSpan;
+
+ // Create a new span for the same operation
+ mixins.beforeMount.call(mockVueInstance);
+
+ // Verify old span was ended and new span was created
+ expect(oldSpan.end).toHaveBeenCalled();
+ expect(mockVueInstance.$_sentryComponentSpans.mount).not.toBe(oldSpan);
+ });
+
+ it('should gracefully handle when "after" hook is called without "before" hook', () => {
+ const mixins = createTracingMixins();
+
+ // Call mounted hook without calling beforeMount first
+ expect(() => mixins.mounted.call(mockVueInstance)).not.toThrow();
+ });
+
+ it('should skip spans when no active root component span (transaction) exists', () => {
+ const mixins = createTracingMixins({ trackComponents: true });
+
+ // Remove active spans
+ (getActiveSpan as any).mockReturnValue(null);
+ mockRootInstance.$_sentryRootComponentSpan = null;
+
+ // Try to create a span
+ mixins.beforeMount.call(mockVueInstance);
+
+ // No span should be created
+ expect(startInactiveSpan).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Component Tracking Options', () => {
+ it.each([
+ { trackComponents: undefined, expected: false, description: 'defaults to not tracking components' },
+ { trackComponents: false, expected: false, description: 'does not track when explicitly disabled' },
+ ])('$description', ({ trackComponents }) => {
+ const mixins = createTracingMixins({ trackComponents });
+ mixins.beforeMount.call(mockVueInstance);
+ expect(startInactiveSpan).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ { trackComponents: true, description: 'tracks all components when enabled' },
+ { trackComponents: ['TestComponent'], description: 'tracks components that match the name list' },
+ ])('$description', ({ trackComponents }) => {
+ const mixins = createTracingMixins({ trackComponents });
+ mixins.beforeMount.call(mockVueInstance);
+ expect(startInactiveSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'Vue TestComponent',
+ op: 'ui.vue.mount',
+ }),
+ );
+ });
+
+ it('does not track components not in the tracking list', () => {
+ const mixins = createTracingMixins({ trackComponents: ['OtherComponent'] });
+ mixins.beforeMount.call(mockVueInstance); // TestComponent
+ expect(startInactiveSpan).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index a547a6d053a5..f8ee2a1e749b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -80,41 +80,40 @@
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8"
integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==
-"@ai-sdk/provider-utils@2.0.2":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.0.2.tgz#ea9d510be442b38bd40ae50dbf5b64ffc396952b"
- integrity sha512-IAvhKhdlXqiSmvx/D4uNlFYCl8dWT+M9K+IuEcSgnE2Aj27GWu8sDIpAf4r4Voc+wOUkOECVKQhFo8g9pozdjA==
+"@ai-sdk/provider-utils@2.2.8":
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz#ad11b92d5a1763ab34ba7b5fc42494bfe08b76d1"
+ integrity sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==
dependencies:
- "@ai-sdk/provider" "1.0.1"
- eventsource-parser "^3.0.0"
- nanoid "^3.3.7"
+ "@ai-sdk/provider" "1.1.3"
+ nanoid "^3.3.8"
secure-json-parse "^2.7.0"
-"@ai-sdk/provider@1.0.1":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-1.0.1.tgz#8172a3cbbfa61bb40b88512165f70fe3c186cb60"
- integrity sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==
+"@ai-sdk/provider@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-1.1.3.tgz#ebdda8077b8d2b3f290dcba32c45ad19b2704681"
+ integrity sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==
dependencies:
json-schema "^0.4.0"
-"@ai-sdk/react@1.0.3":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-1.0.3.tgz#b9bc24e20bdc5768cbb0d9c65471fb60ab2675ec"
- integrity sha512-Mak7qIRlbgtP4I7EFoNKRIQTlABJHhgwrN8SV2WKKdmsfWK2RwcubQWz1hp88cQ0bpF6KxxjSY1UUnS/S9oR5g==
+"@ai-sdk/react@1.2.12":
+ version "1.2.12"
+ resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-1.2.12.tgz#f4250b6df566b170af98a71d5708b52108dd0ce1"
+ integrity sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==
dependencies:
- "@ai-sdk/provider-utils" "2.0.2"
- "@ai-sdk/ui-utils" "1.0.2"
+ "@ai-sdk/provider-utils" "2.2.8"
+ "@ai-sdk/ui-utils" "1.2.11"
swr "^2.2.5"
throttleit "2.1.0"
-"@ai-sdk/ui-utils@1.0.2":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-1.0.2.tgz#2b5ad527f821b055663ddc60f2c45a82956091a0"
- integrity sha512-hHrUdeThGHu/rsGZBWQ9PjrAU9Htxgbo9MFyR5B/aWoNbBeXn1HLMY1+uMEnXL5pRPlmyVRjgIavWg7UgeNDOw==
+"@ai-sdk/ui-utils@1.2.11":
+ version "1.2.11"
+ resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz#4f815589d08d8fef7292ade54ee5db5d09652603"
+ integrity sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==
dependencies:
- "@ai-sdk/provider" "1.0.1"
- "@ai-sdk/provider-utils" "2.0.2"
- zod-to-json-schema "^3.23.5"
+ "@ai-sdk/provider" "1.1.3"
+ "@ai-sdk/provider-utils" "2.2.8"
+ zod-to-json-schema "^3.24.1"
"@ampproject/remapping@2.2.0":
version "2.2.0"
@@ -2965,11 +2964,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353"
integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==
-"@esbuild/aix-ppc64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461"
- integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==
-
"@esbuild/aix-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18"
@@ -3010,11 +3004,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018"
integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==
-"@esbuild/android-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894"
- integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==
-
"@esbuild/android-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f"
@@ -3060,11 +3049,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee"
integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==
-"@esbuild/android-arm@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3"
- integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==
-
"@esbuild/android-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26"
@@ -3105,11 +3089,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517"
integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==
-"@esbuild/android-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb"
- integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==
-
"@esbuild/android-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff"
@@ -3150,11 +3129,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16"
integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==
-"@esbuild/darwin-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936"
- integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==
-
"@esbuild/darwin-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34"
@@ -3195,11 +3169,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931"
integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==
-"@esbuild/darwin-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9"
- integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==
-
"@esbuild/darwin-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418"
@@ -3240,11 +3209,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc"
integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==
-"@esbuild/freebsd-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00"
- integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==
-
"@esbuild/freebsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c"
@@ -3285,11 +3249,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730"
integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==
-"@esbuild/freebsd-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f"
- integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==
-
"@esbuild/freebsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f"
@@ -3330,11 +3289,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383"
integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==
-"@esbuild/linux-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43"
- integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==
-
"@esbuild/linux-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8"
@@ -3375,11 +3329,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771"
integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==
-"@esbuild/linux-arm@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736"
- integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==
-
"@esbuild/linux-arm@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911"
@@ -3420,11 +3369,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333"
integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==
-"@esbuild/linux-ia32@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5"
- integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==
-
"@esbuild/linux-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783"
@@ -3475,11 +3419,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac"
integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==
-"@esbuild/linux-loong64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc"
- integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==
-
"@esbuild/linux-loong64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506"
@@ -3520,11 +3459,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6"
integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==
-"@esbuild/linux-mips64el@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb"
- integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==
-
"@esbuild/linux-mips64el@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96"
@@ -3565,11 +3499,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96"
integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==
-"@esbuild/linux-ppc64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412"
- integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==
-
"@esbuild/linux-ppc64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9"
@@ -3610,11 +3539,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7"
integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==
-"@esbuild/linux-riscv64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694"
- integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==
-
"@esbuild/linux-riscv64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e"
@@ -3655,11 +3579,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f"
integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==
-"@esbuild/linux-s390x@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577"
- integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==
-
"@esbuild/linux-s390x@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d"
@@ -3700,21 +3619,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24"
integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==
-"@esbuild/linux-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f"
- integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==
-
"@esbuild/linux-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4"
integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==
-"@esbuild/netbsd-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6"
- integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==
-
"@esbuild/netbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d"
@@ -3755,11 +3664,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653"
integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==
-"@esbuild/netbsd-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40"
- integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==
-
"@esbuild/netbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79"
@@ -3770,11 +3674,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7"
integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==
-"@esbuild/openbsd-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f"
- integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==
-
"@esbuild/openbsd-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd"
@@ -3815,11 +3714,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273"
integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==
-"@esbuild/openbsd-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205"
- integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==
-
"@esbuild/openbsd-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0"
@@ -3860,11 +3754,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403"
integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==
-"@esbuild/sunos-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6"
- integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==
-
"@esbuild/sunos-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5"
@@ -3905,11 +3794,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2"
integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==
-"@esbuild/win32-arm64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85"
- integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==
-
"@esbuild/win32-arm64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e"
@@ -3950,11 +3834,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac"
integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==
-"@esbuild/win32-ia32@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2"
- integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==
-
"@esbuild/win32-ia32@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d"
@@ -3995,11 +3874,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699"
integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==
-"@esbuild/win32-x64@0.24.2":
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b"
- integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==
-
"@esbuild/win32-x64@0.25.5":
version "0.25.5"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1"
@@ -9648,18 +9522,17 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
-ai@^4.0.6:
- version "4.0.6"
- resolved "https://registry.yarnpkg.com/ai/-/ai-4.0.6.tgz#94ef793df8525c01043e15a60030ce88d7b5c7d5"
- integrity sha512-TD7fH0LymjIYWmdQViB5SoBb1iuuDPOZ7RMU3W9r4SeUf68RzWyixz118QHQTENNqPiGA6vs5NDVAmZOnhzqYA==
+ai@^4.3.16:
+ version "4.3.16"
+ resolved "https://registry.yarnpkg.com/ai/-/ai-4.3.16.tgz#c9446da1024cdc1dfe2913d151b70c91d40f2378"
+ integrity sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==
dependencies:
- "@ai-sdk/provider" "1.0.1"
- "@ai-sdk/provider-utils" "2.0.2"
- "@ai-sdk/react" "1.0.3"
- "@ai-sdk/ui-utils" "1.0.2"
+ "@ai-sdk/provider" "1.1.3"
+ "@ai-sdk/provider-utils" "2.2.8"
+ "@ai-sdk/react" "1.2.12"
+ "@ai-sdk/ui-utils" "1.2.11"
"@opentelemetry/api" "1.9.0"
jsondiffpatch "0.6.0"
- zod-to-json-schema "^3.23.5"
ajv-formats@2.1.1, ajv-formats@^2.1.1:
version "2.1.1"
@@ -15091,37 +14964,6 @@ esbuild@^0.23.0, esbuild@^0.23.1:
"@esbuild/win32-ia32" "0.23.1"
"@esbuild/win32-x64" "0.23.1"
-esbuild@^0.24.2:
- version "0.24.2"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.24.2.tgz#b5b55bee7de017bff5fb8a4e3e44f2ebe2c3567d"
- integrity sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==
- optionalDependencies:
- "@esbuild/aix-ppc64" "0.24.2"
- "@esbuild/android-arm" "0.24.2"
- "@esbuild/android-arm64" "0.24.2"
- "@esbuild/android-x64" "0.24.2"
- "@esbuild/darwin-arm64" "0.24.2"
- "@esbuild/darwin-x64" "0.24.2"
- "@esbuild/freebsd-arm64" "0.24.2"
- "@esbuild/freebsd-x64" "0.24.2"
- "@esbuild/linux-arm" "0.24.2"
- "@esbuild/linux-arm64" "0.24.2"
- "@esbuild/linux-ia32" "0.24.2"
- "@esbuild/linux-loong64" "0.24.2"
- "@esbuild/linux-mips64el" "0.24.2"
- "@esbuild/linux-ppc64" "0.24.2"
- "@esbuild/linux-riscv64" "0.24.2"
- "@esbuild/linux-s390x" "0.24.2"
- "@esbuild/linux-x64" "0.24.2"
- "@esbuild/netbsd-arm64" "0.24.2"
- "@esbuild/netbsd-x64" "0.24.2"
- "@esbuild/openbsd-arm64" "0.24.2"
- "@esbuild/openbsd-x64" "0.24.2"
- "@esbuild/sunos-x64" "0.24.2"
- "@esbuild/win32-arm64" "0.24.2"
- "@esbuild/win32-ia32" "0.24.2"
- "@esbuild/win32-x64" "0.24.2"
-
esbuild@^0.25.0:
version "0.25.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430"
@@ -15537,11 +15379,6 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
-eventsource-parser@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57"
- integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==
-
exec-sh@^0.3.2, exec-sh@^0.3.4:
version "0.3.6"
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc"
@@ -15940,7 +15777,7 @@ fbjs@^0.8.0:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
-fdir@^6.2.0, fdir@^6.3.0, fdir@^6.4.2, fdir@^6.4.4:
+fdir@^6.2.0, fdir@^6.3.0, fdir@^6.4.4:
version "6.4.5"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.5.tgz#328e280f3a23699362f95f2e82acf978a0c0cb49"
integrity sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==
@@ -21553,7 +21390,7 @@ named-placeholders@^1.1.3:
dependencies:
lru-cache "^7.14.1"
-nanoid@^3.3.11, nanoid@^3.3.3, nanoid@^3.3.4, nanoid@^3.3.6, nanoid@^3.3.7, nanoid@^3.3.8:
+nanoid@^3.3.11, nanoid@^3.3.3, nanoid@^3.3.4, nanoid@^3.3.6, nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
@@ -24192,7 +24029,7 @@ postcss@8.4.31:
picocolors "^1.0.0"
source-map-js "^1.0.2"
-postcss@^8.1.10, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.3.7, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.39, postcss@^8.4.43, postcss@^8.4.47, postcss@^8.4.7, postcss@^8.4.8, postcss@^8.5.2, postcss@^8.5.3:
+postcss@^8.1.10, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.3.7, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.39, postcss@^8.4.43, postcss@^8.4.47, postcss@^8.4.7, postcss@^8.4.8, postcss@^8.5.3:
version "8.5.4"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.4.tgz#d61014ac00e11d5f58458ed7247d899bd65f99c0"
integrity sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==
@@ -25782,7 +25619,7 @@ rollup@^3.27.1, rollup@^3.28.1:
optionalDependencies:
fsevents "~2.3.2"
-rollup@^4.18.0, rollup@^4.20.0, rollup@^4.30.1, rollup@^4.34.9, rollup@^4.35.0:
+rollup@^4.18.0, rollup@^4.20.0, rollup@^4.34.9, rollup@^4.35.0:
version "4.41.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.41.1.tgz#46ddc1b33cf1b0baa99320d3b0b4973dc2253b6a"
integrity sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==
@@ -30253,10 +30090,10 @@ zip-stream@^6.0.1:
compress-commons "^6.0.2"
readable-stream "^4.0.0"
-zod-to-json-schema@^3.23.5:
- version "3.23.5"
- resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz#ec23def47dcafe3a4d640eba6a346b34f9a693a5"
- integrity sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==
+zod-to-json-schema@^3.24.1:
+ version "3.24.5"
+ resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
+ integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
zod@^3.22.3, zod@^3.22.4, zod@^3.24.1:
version "3.24.1"