>[1]) => {
+ await sleep();
+ await next();
+ await sleep();
+ },
+];
+
+export async function loader() {
+ await sleep();
+}
+
+export default function Index() {
+ return (
+
+
Welcome to React Router
+
+ );
+}
diff --git a/playground/observability/app/routes/slug.tsx b/playground/observability/app/routes/slug.tsx
new file mode 100644
index 0000000000..ce58440893
--- /dev/null
+++ b/playground/observability/app/routes/slug.tsx
@@ -0,0 +1,46 @@
+import { startMeasure } from "~/o11y";
+import { type Route } from "../../.react-router/types/app/routes/+types/slug";
+
+let sleep = (ms: number = Math.max(100, Math.round(Math.random() * 500))) =>
+ new Promise((r) => setTimeout(r, ms));
+
+export const middleware: Route.MiddlewareFunction[] = [
+ async (_, next) => {
+ await sleep();
+ await next();
+ await sleep();
+ },
+];
+
+export const clientMiddleware: Route.ClientMiddlewareFunction[] = [
+ async (_, next) => {
+ await sleep();
+ await next();
+ await sleep();
+ },
+];
+
+export async function loader({ params }: Route.LoaderArgs) {
+ await sleep();
+ return params.slug;
+}
+
+export async function clientLoader({
+ serverLoader,
+ pattern,
+}: Route.ClientLoaderArgs) {
+ await sleep();
+ let end = startMeasure(["serverLoader", pattern]);
+ let value = await serverLoader();
+ end();
+ await sleep();
+ return value;
+}
+
+export default function Slug({ loaderData }: Route.ComponentProps) {
+ return (
+
+
Slug: {loaderData}
+
+ );
+}
diff --git a/playground/observability/package.json b/playground/observability/package.json
new file mode 100644
index 0000000000..c0288d54a4
--- /dev/null
+++ b/playground/observability/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@playground/framework-express",
+ "version": "0.0.0",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "build": "react-router build",
+ "dev": "node ./server.js",
+ "start": "cross-env NODE_ENV=production node ./server.js",
+ "typecheck": "react-router typegen && tsc"
+ },
+ "dependencies": {
+ "@react-router/express": "workspace:*",
+ "@react-router/node": "workspace:*",
+ "compression": "^1.7.4",
+ "express": "^4.19.2",
+ "isbot": "^5.1.11",
+ "morgan": "^1.10.0",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-router": "workspace:*"
+ },
+ "devDependencies": {
+ "@react-router/dev": "workspace:*",
+ "@types/compression": "^1.7.5",
+ "@types/express": "^4.17.20",
+ "@types/morgan": "^1.9.9",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "cross-env": "^7.0.3",
+ "typescript": "^5.1.6",
+ "vite": "^6.1.0",
+ "vite-tsconfig-paths": "^4.2.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/playground/observability/public/favicon.ico b/playground/observability/public/favicon.ico
new file mode 100644
index 0000000000..5dbdfcddcb
Binary files /dev/null and b/playground/observability/public/favicon.ico differ
diff --git a/playground/observability/react-router.config.ts b/playground/observability/react-router.config.ts
new file mode 100644
index 0000000000..039108a6a7
--- /dev/null
+++ b/playground/observability/react-router.config.ts
@@ -0,0 +1,7 @@
+import type { Config } from "@react-router/dev/config";
+
+export default {
+ future: {
+ v8_middleware: true,
+ },
+} satisfies Config;
diff --git a/playground/observability/server.js b/playground/observability/server.js
new file mode 100644
index 0000000000..fa5048f32c
--- /dev/null
+++ b/playground/observability/server.js
@@ -0,0 +1,43 @@
+import { createRequestHandler } from "@react-router/express";
+import compression from "compression";
+import express from "express";
+import morgan from "morgan";
+
+const viteDevServer =
+ process.env.NODE_ENV === "production"
+ ? undefined
+ : await import("vite").then((vite) =>
+ vite.createServer({
+ server: { middlewareMode: true },
+ })
+ );
+
+const reactRouterHandler = createRequestHandler({
+ build: viteDevServer
+ ? () => viteDevServer.ssrLoadModule("virtual:react-router/server-build")
+ : await import("./build/server/index.js"),
+});
+
+const app = express();
+
+app.use(compression());
+app.disable("x-powered-by");
+
+if (viteDevServer) {
+ app.use(viteDevServer.middlewares);
+} else {
+ app.use(
+ "/assets",
+ express.static("build/client/assets", { immutable: true, maxAge: "1y" })
+ );
+}
+
+app.use(express.static("build/client", { maxAge: "1h" }));
+app.use(morgan("tiny"));
+
+app.all("*", reactRouterHandler);
+
+const port = process.env.PORT || 3000;
+app.listen(port, () =>
+ console.log(`Express server listening at http://localhost:${port}`)
+);
diff --git a/playground/observability/tsconfig.json b/playground/observability/tsconfig.json
new file mode 100644
index 0000000000..79cf7b5af6
--- /dev/null
+++ b/playground/observability/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/.server/**/*.ts",
+ "**/.server/**/*.tsx",
+ "**/.client/**/*.ts",
+ "**/.client/**/*.tsx",
+ "./.react-router/types/**/*"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@react-router/node", "vite/client"],
+ "verbatimModuleSyntax": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "target": "ES2022",
+ "strict": true,
+ "allowJs": true,
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true,
+ "rootDirs": [".", "./.react-router/types"]
+ }
+}
diff --git a/playground/observability/vite.config.ts b/playground/observability/vite.config.ts
new file mode 100644
index 0000000000..f910ad4c18
--- /dev/null
+++ b/playground/observability/vite.config.ts
@@ -0,0 +1,7 @@
+import { reactRouter } from "@react-router/dev/vite";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [reactRouter(), tsconfigPaths()],
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 182ceea34b..18a86c76fd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1688,6 +1688,67 @@ importers:
specifier: ^4.2.1
version: 4.3.2(typescript@5.4.5)(vite@6.2.5(@types/node@20.11.30)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))
+ playground/observability:
+ dependencies:
+ '@react-router/express':
+ specifier: workspace:*
+ version: link:../../packages/react-router-express
+ '@react-router/node':
+ specifier: workspace:*
+ version: link:../../packages/react-router-node
+ compression:
+ specifier: ^1.7.4
+ version: 1.8.0
+ express:
+ specifier: ^4.19.2
+ version: 4.21.2
+ isbot:
+ specifier: ^5.1.11
+ version: 5.1.11
+ morgan:
+ specifier: ^1.10.0
+ version: 1.10.0
+ react:
+ specifier: ^19.1.0
+ version: 19.1.0
+ react-dom:
+ specifier: ^19.1.0
+ version: 19.1.0(react@19.1.0)
+ react-router:
+ specifier: workspace:*
+ version: link:../../packages/react-router
+ devDependencies:
+ '@react-router/dev':
+ specifier: workspace:*
+ version: link:../../packages/react-router-dev
+ '@types/compression':
+ specifier: ^1.7.5
+ version: 1.7.5
+ '@types/express':
+ specifier: ^4.17.20
+ version: 4.17.21
+ '@types/morgan':
+ specifier: ^1.9.9
+ version: 1.9.9
+ '@types/react':
+ specifier: ^18.2.18
+ version: 18.2.18
+ '@types/react-dom':
+ specifier: ^18.2.7
+ version: 18.2.7
+ cross-env:
+ specifier: ^7.0.3
+ version: 7.0.3
+ typescript:
+ specifier: ^5.1.6
+ version: 5.4.5
+ vite:
+ specifier: ^6.1.0
+ version: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0)
+ vite-tsconfig-paths:
+ specifier: ^4.2.1
+ version: 4.3.2(typescript@5.4.5)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.3)(yaml@2.8.0))
+
playground/rsc-parcel:
dependencies:
'@mjackson/node-fetch-server':