> = {
bundle_replay_min: 'build/bundles/bundle.replay.min.js',
bundle_tracing: 'build/bundles/bundle.tracing.js',
bundle_tracing_min: 'build/bundles/bundle.tracing.min.js',
+ bundle_tracing_logs_metrics: 'build/bundles/bundle.tracing.logs.metrics.js',
+ bundle_tracing_logs_metrics_min: 'build/bundles/bundle.tracing.logs.metrics.min.js',
+ bundle_tracing_logs_metrics_debug_min: 'build/bundles/bundle.tracing.logs.metrics.debug.min.js',
bundle_tracing_replay: 'build/bundles/bundle.tracing.replay.js',
bundle_tracing_replay_min: 'build/bundles/bundle.tracing.replay.min.js',
bundle_tracing_replay_feedback: 'build/bundles/bundle.tracing.replay.feedback.js',
@@ -245,7 +248,9 @@ class SentryScenarioGenerationPlugin {
.replace('loader_', 'bundle_')
.replace('_replay', '')
.replace('_tracing', '')
- .replace('_feedback', '');
+ .replace('_feedback', '')
+ .replace('_logs', '')
+ .replace('_metrics', '');
// For feedback bundle, make sure to add modal & screenshot integrations
if (bundleKey.includes('_feedback')) {
diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts
index 0888b3e286b0..6cc5188d3c29 100644
--- a/dev-packages/browser-integration-tests/utils/helpers.ts
+++ b/dev-packages/browser-integration-tests/utils/helpers.ts
@@ -314,6 +314,30 @@ export function shouldSkipTracingTest(): boolean {
return bundle != null && !bundle.includes('tracing') && !bundle.includes('esm') && !bundle.includes('cjs');
}
+/**
+ * We can only test metrics tests in certain bundles/packages:
+ * - NPM (ESM, CJS)
+ * - CDN bundles that contain metrics
+ *
+ * @returns `true` if we should skip the metrics test
+ */
+export function shouldSkipMetricsTest(): boolean {
+ const bundle = process.env.PW_BUNDLE;
+ return bundle != null && !bundle.includes('metrics') && !bundle.includes('esm') && !bundle.includes('cjs');
+}
+
+/**
+ * We can only test logs tests in certain bundles/packages:
+ * - NPM (ESM, CJS)
+ * - CDN bundles that contain logs
+ *
+ * @returns `true` if we should skip the logs test
+ */
+export function shouldSkipLogsTest(): boolean {
+ const bundle = process.env.PW_BUNDLE;
+ return bundle != null && !bundle.includes('logs') && !bundle.includes('esm') && !bundle.includes('cjs');
+}
+
/**
* @returns `true` if we are testing a CDN bundle
*/
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
index e14573254dfb..8616aafadba8 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
@@ -17,7 +17,6 @@ test('Will create a transaction with spans for every server component and metada
expect(spanDescriptions).toContainEqual('render route (app) /nested-layout');
expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page');
- expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)');
// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans
if (!isNext13) {
@@ -46,7 +45,6 @@ test('Will create a transaction with spans for every server component and metada
expect(spanDescriptions).toContainEqual('render route (app) /nested-layout/[dynamic]');
expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page');
- expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout/[dynamic])');
// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans
if (!isNext13) {
diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
index 40da7f5fb859..1ec7d2833a65 100644
--- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
+++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json
@@ -21,7 +21,7 @@
"@sentry/remix": "latest || *",
"@sentry/vite-plugin": "^4.6.1",
"@shopify/hydrogen": "2025.4.0",
- "@shopify/remix-oxygen": "^2.0.10",
+ "@shopify/remix-oxygen": "2.0.10",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"isbot": "^3.8.0",
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts
new file mode 100644
index 000000000000..daf81ea97e10
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/middleware.ts
@@ -0,0 +1,55 @@
+import { createMiddleware } from '@tanstack/react-start';
+import { wrapMiddlewaresWithSentry } from '@sentry/tanstackstart-react';
+
+// Global request middleware - runs on every request
+const globalRequestMiddleware = createMiddleware().server(async ({ next }) => {
+ console.log('Global request middleware executed');
+ return next();
+});
+
+// Global function middleware - runs on every server function
+const globalFunctionMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ console.log('Global function middleware executed');
+ return next();
+});
+
+// Server function middleware
+const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ console.log('Server function middleware executed');
+ return next();
+});
+
+// Server route request middleware
+const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
+ console.log('Server route request middleware executed');
+ return next();
+});
+
+// Early return middleware - returns without calling next()
+const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => {
+ console.log('Early return middleware executed - not calling next()');
+ return { earlyReturn: true, message: 'Middleware returned early without calling next()' };
+});
+
+// Error middleware - throws an exception
+const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => {
+ console.log('Error middleware executed - throwing error');
+ throw new Error('Middleware Error Test');
+});
+
+// Manually wrap middlewares with Sentry
+export const [
+ wrappedGlobalRequestMiddleware,
+ wrappedGlobalFunctionMiddleware,
+ wrappedServerFnMiddleware,
+ wrappedServerRouteRequestMiddleware,
+ wrappedEarlyReturnMiddleware,
+ wrappedErrorMiddleware,
+] = wrapMiddlewaresWithSentry({
+ globalRequestMiddleware,
+ globalFunctionMiddleware,
+ serverFnMiddleware,
+ serverRouteRequestMiddleware,
+ earlyReturnMiddleware,
+ errorMiddleware,
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts
new file mode 100644
index 000000000000..1bf3fdb1c5da
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.test-middleware.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { wrappedServerRouteRequestMiddleware } from '../middleware';
+
+export const Route = createFileRoute('/api/test-middleware')({
+ server: {
+ middleware: [wrappedServerRouteRequestMiddleware],
+ handlers: {
+ GET: async () => {
+ return { message: 'Server route middleware test' };
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx
new file mode 100644
index 000000000000..83ac81c75a62
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/test-middleware.tsx
@@ -0,0 +1,86 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { createServerFn } from '@tanstack/react-start';
+import { wrappedServerFnMiddleware, wrappedEarlyReturnMiddleware, wrappedErrorMiddleware } from '../middleware';
+
+// Server function with specific middleware (also gets global function middleware)
+const serverFnWithMiddleware = createServerFn()
+ .middleware([wrappedServerFnMiddleware])
+ .handler(async () => {
+ console.log('Server function with specific middleware executed');
+ return { message: 'Server function middleware test' };
+ });
+
+// Server function without specific middleware (only gets global function middleware)
+const serverFnWithoutMiddleware = createServerFn().handler(async () => {
+ console.log('Server function without specific middleware executed');
+ return { message: 'Global middleware only test' };
+});
+
+// Server function with early return middleware (middleware returns without calling next)
+const serverFnWithEarlyReturnMiddleware = createServerFn()
+ .middleware([wrappedEarlyReturnMiddleware])
+ .handler(async () => {
+ console.log('This should not be executed - middleware returned early');
+ return { message: 'This should not be returned' };
+ });
+
+// Server function with error middleware (middleware throws an error)
+const serverFnWithErrorMiddleware = createServerFn()
+ .middleware([wrappedErrorMiddleware])
+ .handler(async () => {
+ console.log('This should not be executed - middleware threw error');
+ return { message: 'This should not be returned' };
+ });
+
+export const Route = createFileRoute('/test-middleware')({
+ component: TestMiddleware,
+});
+
+function TestMiddleware() {
+ return (
+
+
Test Middleware Page
+
+
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/start.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/start.ts
new file mode 100644
index 000000000000..eecd2816e492
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/start.ts
@@ -0,0 +1,9 @@
+import { createStart } from '@tanstack/react-start';
+import { wrappedGlobalRequestMiddleware, wrappedGlobalFunctionMiddleware } from './middleware';
+
+export const startInstance = createStart(() => {
+ return {
+ requestMiddleware: [wrappedGlobalRequestMiddleware],
+ functionMiddleware: [wrappedGlobalFunctionMiddleware],
+ };
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts
new file mode 100644
index 000000000000..824a611bc2ae
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts
@@ -0,0 +1,192 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({
+ page,
+}) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-middleware-btn')).toBeVisible();
+ await page.locator('#server-fn-middleware-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Find both middleware spans
+ const serverFnMiddlewareSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'serverFnMiddleware' && span.origin === 'manual.middleware.tanstackstart',
+ );
+ const globalFunctionMiddlewareSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'globalFunctionMiddleware' && span.origin === 'manual.middleware.tanstackstart',
+ );
+
+ // Verify both middleware spans exist with expected properties
+ expect(serverFnMiddlewareSpan).toEqual(
+ expect.objectContaining({
+ description: 'serverFnMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ );
+ expect(globalFunctionMiddlewareSpan).toEqual(
+ expect.objectContaining({
+ description: 'globalFunctionMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ );
+
+ // Both middleware spans should be siblings under the same parent
+ expect(serverFnMiddlewareSpan?.parent_span_id).toBe(globalFunctionMiddlewareSpan?.parent_span_id);
+});
+
+test('Sends spans for global function middleware', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-global-only-btn')).toBeVisible();
+ await page.locator('#server-fn-global-only-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the global function middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'globalFunctionMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends spans for global request middleware', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-middleware'
+ );
+ });
+
+ await page.goto('/test-middleware');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the global request middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'globalRequestMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends spans for server route request middleware', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /api/test-middleware'
+ );
+ });
+
+ await page.goto('/api/test-middleware');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the server route request middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'serverRouteRequestMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends span for middleware that returns early without calling next()', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-early-return-btn')).toBeVisible();
+ await page.locator('#server-fn-early-return-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the early return middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'earlyReturnMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends span for middleware that throws an error', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-error-btn')).toBeVisible();
+ await page.locator('#server-fn-error-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the error middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'errorMiddleware',
+ op: 'middleware.tanstackstart',
+ origin: 'manual.middleware.tanstackstart',
+ }),
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts
index d2ebbffb0ec0..3ef96e887bd2 100644
--- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts
@@ -84,17 +84,19 @@ test('Sends a server function transaction for a nested server function only if i
]),
);
- // Verify that the auto span is the parent of the nested span
- const autoSpan = transactionEvent?.spans?.find(
- (span: { op?: string; origin?: string }) =>
- span.op === 'function.tanstackstart' && span.origin === 'auto.function.tanstackstart.server',
+ // Verify that globalFunctionMiddleware and testNestedLog are sibling spans under the root
+ const functionMiddlewareSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'globalFunctionMiddleware' && span.origin === 'manual.middleware.tanstackstart',
);
const nestedSpan = transactionEvent?.spans?.find(
(span: { description?: string; origin?: string }) =>
span.description === 'testNestedLog' && span.origin === 'manual',
);
- expect(autoSpan).toBeDefined();
+ expect(functionMiddlewareSpan).toBeDefined();
expect(nestedSpan).toBeDefined();
- expect(nestedSpan?.parent_span_id).toBe(autoSpan?.span_id);
+
+ // Both spans should be siblings under the same parent (root transaction)
+ expect(nestedSpan?.parent_span_id).toBe(functionMiddlewareSpan?.parent_span_id);
});
diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts
index 1eda48705b08..ef3e721dc09e 100644
--- a/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts
+++ b/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts
@@ -22,8 +22,16 @@ const instanceMap: WeakMap