Skip to content

remix: Trailing-underscore route segments leak into transaction names #24119

Description

@suhailopensource

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/remix

SDK Version

10.73.0

Framework Version

Remix 2.17.4

Link to Sentry event

No response

Reproduction Example/SDK Setup

Any Remix v2 app using the trailing-underscore convention reproduces this. Given app/routes/concerts_.mine.tsx, requesting /concerts/mine produces a server transaction named /concerts_/mine.

A unit test in this repo reproduces it without a server. Save as packages/remix/test/utils/trailing-underscore.test.ts and run cd packages/remix && yarn test:unit --run test/utils/trailing-underscore.test.ts:

import type { AgnosticRouteObject } from '@remix-run/router';
import { describe, expect, it } from 'vitest';
import { convertRemixRouteIdToPath, getTransactionName } from '../../src/utils/utils';

describe('trailing-underscore routes', () => {
  // `path` is what Remix itself computes for these route files.
  const routes: AgnosticRouteObject[] = [
    { id: 'routes/concerts_.mine', path: '/concerts/mine' },
    { id: 'routes/app_.projects.$id.roadmap', path: '/app/projects/:id/roadmap' },
    { id: 'routes/concerts.$city', path: '/concerts/:city' },
  ];

  it('convertRemixRouteIdToPath strips a trailing underscore', () => {
    expect(convertRemixRouteIdToPath('routes/concerts_.mine')).toBe('/concerts/mine');
    expect(convertRemixRouteIdToPath('routes/app_.projects.$id.roadmap')).toBe('/app/projects/:id/roadmap');
    // controls, already correct today
    expect(convertRemixRouteIdToPath('routes/concerts.$city')).toBe('/concerts/:city');
    expect(convertRemixRouteIdToPath('routes/_layout.dashboard')).toBe('/dashboard');
  });

  it('getTransactionName names the server transaction correctly', () => {
    expect(getTransactionName(routes, new URL('http://localhost/concerts/mine'))).toEqual(['/concerts/mine', 'route']);
    expect(getTransactionName(routes, new URL('http://localhost/app/projects/42/roadmap'))).toEqual([
      '/app/projects/:id/roadmap',
      'route',
    ]);
  });
});

Steps to Reproduce

Any Remix v2 app using the trailing-underscore convention reproduces this. Given app/routes/concerts_.mine.tsx, requesting /concerts/mine produces a server transaction named /concerts_/mine.

A unit test in this repo reproduces it without a server. Save as packages/remix/test/utils/trailing-underscore.test.ts and run cd packages/remix && yarn test:unit --run test/utils/trailing-underscore.test.ts:

import type { AgnosticRouteObject } from '@remix-run/router';
import { describe, expect, it } from 'vitest';
import { convertRemixRouteIdToPath, getTransactionName } from '../../src/utils/utils';

describe('trailing-underscore routes', () => {
  // `path` is what Remix itself computes for these route files.
  const routes: AgnosticRouteObject[] = [
    { id: 'routes/concerts_.mine', path: '/concerts/mine' },
    { id: 'routes/app_.projects.$id.roadmap', path: '/app/projects/:id/roadmap' },
    { id: 'routes/concerts.$city', path: '/concerts/:city' },
  ];

  it('convertRemixRouteIdToPath strips a trailing underscore', () => {
    expect(convertRemixRouteIdToPath('routes/concerts_.mine')).toBe('/concerts/mine');
    expect(convertRemixRouteIdToPath('routes/app_.projects.$id.roadmap')).toBe('/app/projects/:id/roadmap');
    // controls, already correct today
    expect(convertRemixRouteIdToPath('routes/concerts.$city')).toBe('/concerts/:city');
    expect(convertRemixRouteIdToPath('routes/_layout.dashboard')).toBe('/dashboard');
  });

  it('getTransactionName names the server transaction correctly', () => {
    expect(getTransactionName(routes, new URL('http://localhost/concerts/mine'))).toEqual(['/concerts/mine', 'route']);
    expect(getTransactionName(routes, new URL('http://localhost/app/projects/42/roadmap'))).toEqual([
      '/app/projects/:id/roadmap',
      'route',
    ]);
  });
});

Expected Result

Per the file route conventions, a trailing underscore opts a route out of layout nesting without appearing in the URL:

URL Matched Route
/concerts/mine app/routes/concerts_.mine.tsx

The trailing_ underscore creates a path segment, but it does not create layout nesting.

So routes/concerts_.mine should yield /concerts/mine, and the transaction should be named /concerts/mine.

Actual Result

The underscore is kept as part of the path segment:

route id expected actual
routes/concerts_.mine /concerts/mine /concerts_/mine
routes/app_.projects.$id.roadmap /app/projects/:id/roadmap /app_/projects/:id/roadmap
routes/concerts.$city /concerts/:city /concerts/:city (control, correct)
routes/_layout.dashboard /dashboard /dashboard (control, correct)

Through the real consumer, getTransactionName returns:

/concerts/mine          -> ["/concerts_/mine", "route"]
/app/projects/42/roadmap -> ["/app_/projects/:id/roadmap", "route"]

Both tests above fail. Note the source is route, not url — the name is reported as a high-confidence parameterized route while being wrong.

Additional Context

Where it bites

getTransactionName (packages/remix/src/utils/utils.ts) derives the name from match.route.id and runs it through convertRemixRouteIdToPath, ignoring match.route.path — which Remix has already resolved correctly. It is used for server transaction naming at packages/remix/src/server/instrumentServer.ts:182 and :362.

Because the returned source is route, affected transactions are grouped under a route name that does not correspond to any real URL, so they never group with the same route reported from elsewhere.

The same segment loop is duplicated in packages/remix/src/config/createRemixRouteManifest.ts, so the build-time route manifest carries the same wrong path.

Suggested fix

Strip a trailing _ from each non-dynamic segment, mirroring the adjacent leading-underscore branch — roughly 2 lines in each of the two loops. Existing coverage in packages/remix/test/config/routeConversion.test.ts and packages/remix/test/utils/utils.test.ts covers leading _ and _index but has no trailing-underscore case. Happy to open a PR with the test above.

Affected versions

Present in 10.73.0 (both packages/remix/src/utils/utils.ts and packages/remix/src/config/createRemixRouteManifest.ts) and on current develop.

Priority

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions