Skip to content

Commit a3875c5

Browse files
authored
feat(remix): Add parameterized transaction naming for routes (#17951)
This PR adds parameterized routes for Remix SDK, similarly to the NextJS SDK Resolves: #16690 _Needs docs PR for the Vite plugin_ Adds automatic parameterization of Remix route IDs for transaction naming. Converts internal Remix route file patterns to parameterized paths similar to the other SDKs (`routes/user.$id` -> `/user/:id`). - Created `sentryRemixVitePlugin` that extracts route information from Remix builds and generates a manifest file containing route ID to parameterized route mappings. - Added `remixRouteParameterization.ts` with regex-based route conversion that handles dynamic segments (`$param` → `:param`), splat routes (`$` → `:*`), and nested routes. - Updated both client and server instrumentation in `performance.tsx` and `instrumentServer.ts` to use parameterized transaction names. - It also works with Hydrogen Vite plugin is used like: ```javascript // vite.config.ts import { defineConfig } from 'vite'; import { vitePlugin as remix } from '@remix-run/dev'; import { sentryRemixVitePlugin } from '@sentry/remix'; export default defineConfig({ plugins: [ remix(), sentryRemixVitePlugin({ appDirPath: './app', // Default is 'app' in project root }), ], }); ``` Updated all Remix E2E test applications to expect parameterized route names. Added new E2E test application `create-remix-app-v2-non-vite` for testing backward compatibility for non-Vite builds - these continue to use `routes/user.$id` format. ## Summary | Route File | Example URL | Before | After (Server - All Apps) | After (Client - Vite Plugin) | |------------|-------------|--------|---------------------------|------------------------------| | `_index.tsx` | `/` | `routes/_index` | `/` | `/` | | `user.$id.tsx` | `/user/123` | `routes/user.$id` | `/user/:id` | `/user/:id` | | `users.$userId.posts.$postId.tsx` | `/users/123/posts/456` | `routes/users.$userId.posts.$postId` | `/users/:userId/posts/:postId` | `/users/:userId/posts/:postId` | | `$.tsx` (splat) | `/docs/guide/intro` | `routes/$` | `/:*` | `/:*` | | `docs.$.tsx` (scoped splat) | `/docs/guide/intro` | `routes/docs.$` | `/docs/:*` | `/docs/:*` | ### Notes - **Server-side**: Parameterization works automatically for all Remix apps (Vite, non-Vite, Hydrogen, Express). - **Client-side**: Requires the Vite plugin. Without it, client transactions continue using route IDs (e.g., `routes/user.$id`).
1 parent 3b0728f commit a3875c5

File tree

68 files changed

+3702
-154
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+3702
-154
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ jobs:
810810
needs: [job_get_metadata, job_build]
811811
if: needs.job_build.outputs.changed_remix == 'true' || github.event_name != 'pull_request'
812812
runs-on: ubuntu-24.04
813-
timeout-minutes: 10
813+
timeout-minutes: 15
814814
strategy:
815815
fail-fast: false
816816
matrix:

dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/client-transactions.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
33

44
test('Sends a pageload transaction to Sentry', async ({ page }) => {
55
const transactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => {
6-
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index';
6+
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
77
});
88

99
await page.goto('/');
@@ -15,7 +15,7 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => {
1515

1616
test('Sends a navigation transaction to Sentry', async ({ page }) => {
1717
const transactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => {
18-
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id';
18+
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id';
1919
});
2020

2121
await page.goto('/');
@@ -28,6 +28,22 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
2828
expect(transactionEvent).toBeDefined();
2929
});
3030

31+
test('Sends a navigation transaction with parameterized route to Sentry', async ({ page }) => {
32+
const transactionPromise = waitForTransaction('create-remix-app-express-vite-dev', transactionEvent => {
33+
return transactionEvent.contexts?.trace?.op === 'navigation';
34+
});
35+
36+
await page.goto('/');
37+
38+
const linkElement = page.locator('id=navigation');
39+
await linkElement.click();
40+
41+
const transactionEvent = await transactionPromise;
42+
43+
expect(transactionEvent).toBeDefined();
44+
expect(transactionEvent.transaction).toBeTruthy();
45+
});
46+
3147
test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
3248
await page.goto('/');
3349

dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
4848
const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id;
4949

5050
expect(httpServerTransaction.transaction).toBe('GET http://localhost:3030/');
51-
expect(pageloadTransaction.transaction).toBe('routes/_index');
51+
expect(pageloadTransaction.transaction).toBe('/');
5252

5353
expect(httpServerTraceId).toBeDefined();
5454
expect(httpServerSpanId).toBeDefined();

dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/vite.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import { installGlobals } from '@remix-run/node';
12
import { vitePlugin as remix } from '@remix-run/dev';
3+
import { sentryRemixVitePlugin } from '@sentry/remix';
24
import { defineConfig } from 'vite';
35
import tsconfigPaths from 'vite-tsconfig-paths';
46

5-
import { installGlobals } from '@remix-run/node';
6-
77
installGlobals();
88

99
export default defineConfig({
1010
plugins: [
1111
remix(),
12+
sentryRemixVitePlugin(),
1213
tsconfigPaths({
1314
// The dev server config errors are not relevant to this test app
1415
// https://github.com/aleclarson/vite-tsconfig-paths?tab=readme-ov-file#options

dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/client-transactions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
33

44
test('Sends a pageload transaction to Sentry', async ({ page }) => {
55
const transactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => {
6-
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === 'routes/_index';
6+
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
77
});
88

99
await page.goto('/');
@@ -15,7 +15,7 @@ test('Sends a pageload transaction to Sentry', async ({ page }) => {
1515

1616
test('Sends a navigation transaction to Sentry', async ({ page }) => {
1717
const transactionPromise = waitForTransaction('create-remix-app-express', transactionEvent => {
18-
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === 'routes/user.$id';
18+
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id';
1919
});
2020

2121
await page.goto('/');

dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => {
9090
const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id;
9191

9292
expect(httpServerTransaction.transaction).toBe('GET client-error');
93-
expect(pageloadTransaction.transaction).toBe('routes/client-error');
93+
expect(pageloadTransaction.transaction).toBe('/client-error');
9494

9595
expect(httpServerTraceId).toBeDefined();
9696
expect(httpServerSpanId).toBeDefined();
@@ -132,7 +132,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
132132
const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id;
133133

134134
expect(httpServerTransaction.transaction).toBe('GET http://localhost:3030/');
135-
expect(pageloadTransaction.transaction).toBe('routes/_index');
135+
expect(pageloadTransaction.transaction).toBe('/');
136136

137137
expect(httpServerTraceId).toBeDefined();
138138
expect(httpServerSpanId).toBeDefined();

dev-packages/e2e-tests/test-applications/create-remix-app-express/vite.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import { installGlobals } from '@remix-run/node';
12
import { vitePlugin as remix } from '@remix-run/dev';
3+
import { sentryRemixVitePlugin } from '@sentry/remix';
24
import { defineConfig } from 'vite';
35
import tsconfigPaths from 'vite-tsconfig-paths';
46

5-
import { installGlobals } from '@remix-run/node';
6-
77
installGlobals();
88

99
export default defineConfig({
1010
plugins: [
1111
remix(),
12+
sentryRemixVitePlugin(),
1213
tsconfigPaths({
1314
// The dev server config errors are not relevant to this test app
1415
// https://github.com/aleclarson/vite-tsconfig-paths?tab=readme-ov-file#options
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('eslint').Linter.Config} */
2+
module.exports = {
3+
extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'],
4+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
/public/build
6+
.env
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873

0 commit comments

Comments
 (0)