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
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/mineproduces 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.tsand runcd packages/remix && yarn test:unit --run test/utils/trailing-underscore.test.ts:Steps to Reproduce
Any Remix v2 app using the trailing-underscore convention reproduces this. Given
app/routes/concerts_.mine.tsx, requesting/concerts/mineproduces 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.tsand runcd packages/remix && yarn test:unit --run test/utils/trailing-underscore.test.ts:Expected Result
Per the file route conventions, a trailing underscore opts a route out of layout nesting without appearing in the URL:
/concerts/mineapp/routes/concerts_.mine.tsxSo
routes/concerts_.mineshould yield/concerts/mine, and the transaction should be named/concerts/mine.Actual Result
The underscore is kept as part of the path segment:
routes/concerts_.mine/concerts/mine/concerts_/mineroutes/app_.projects.$id.roadmap/app/projects/:id/roadmap/app_/projects/:id/roadmaproutes/concerts.$city/concerts/:city/concerts/:city(control, correct)routes/_layout.dashboard/dashboard/dashboard(control, correct)Through the real consumer,
getTransactionNamereturns:Both tests above fail. Note the source is
route, noturl— 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 frommatch.route.idand runs it throughconvertRemixRouteIdToPath, ignoringmatch.route.path— which Remix has already resolved correctly. It is used for server transaction naming atpackages/remix/src/server/instrumentServer.ts:182and: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 inpackages/remix/test/config/routeConversion.test.tsandpackages/remix/test/utils/utils.test.tscovers leading_and_indexbut has no trailing-underscore case. Happy to open a PR with the test above.Affected versions
Present in
10.73.0(bothpackages/remix/src/utils/utils.tsandpackages/remix/src/config/createRemixRouteManifest.ts) and on currentdevelop.Priority
No response