Skip to content

Commit f7b6c45

Browse files
authored
feat(react-router): Switch to new stable instrumentations api (#1265)
* add new api * fix tests
1 parent 05045c0 commit f7b6c45

11 files changed

Lines changed: 87 additions & 72 deletions

File tree

e2e-tests/test-applications/react-router-test-app/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
"typecheck": "react-router typegen && tsc"
1111
},
1212
"dependencies": {
13-
"@react-router/dev": "^7.12.0",
14-
"@react-router/node": "^7.12.0",
15-
"@react-router/serve": "^7.12.0",
13+
"@react-router/dev": "^7.15.0",
14+
"@react-router/node": "^7.15.0",
15+
"@react-router/serve": "^7.15.0",
1616
"isbot": "^4.4.0",
1717
"react": "^18.3.1",
1818
"react-dom": "^18.3.1",
19-
"react-router": "^7.12.0"
19+
"react-router": "^7.15.0"
2020
},
2121
"devDependencies": {
2222
"@types/react": "^18.3.9",

e2e-tests/tests/react-router-instrumentation-api.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
//@ts-expect-error - clifty is ESM only
1616
import { KEYS, withEnv } from 'clifty';
1717

18-
// Expects React Router >= 7.9.5
18+
// Expects React Router >= 7.15.0
1919
async function runWizardWithInstrumentationAPI(
2020
projectDir: string,
2121
): Promise<number> {
@@ -90,7 +90,7 @@ describe('React Router Instrumentation API', () => {
9090
'@sentry/react-router',
9191
'const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });',
9292
'integrations: [tracing',
93-
'unstable_instrumentations={[tracing.clientInstrumentation]}',
93+
'instrumentations={[tracing.clientInstrumentation]}',
9494
]);
9595
});
9696

@@ -100,7 +100,7 @@ describe('React Router Instrumentation API', () => {
100100
'@sentry/react-router',
101101
'Sentry.wrapSentryHandleRequest(',
102102
'export const handleError = Sentry.createSentryHandleError(',
103-
'export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()]',
103+
'export const instrumentations = [Sentry.createSentryServerInstrumentation()]',
104104
]);
105105
});
106106

src/react-router/codemods/client.entry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Sentry.init({
112112
props.push('onError={Sentry.sentryOnError}');
113113
}
114114
if (addInstrProp) {
115-
props.push('unstable_instrumentations={[tracing.clientInstrumentation]}');
115+
props.push('instrumentations={[tracing.clientInstrumentation]}');
116116
}
117117
clack.log.warn(
118118
`Could not find ${chalk.cyan(
@@ -201,7 +201,7 @@ function addOnErrorToHydratedRouter(ast: t.Program): boolean {
201201
function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {
202202
return addPropToHydratedRouter(
203203
ast,
204-
'unstable_instrumentations',
204+
'instrumentations',
205205
recast.types.builders.arrayExpression([
206206
recast.types.builders.memberExpression(
207207
recast.types.builders.identifier('tracing'),

src/react-router/codemods/server-entry.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,54 +43,51 @@ export async function instrumentServerEntry(
4343
instrumentHandleRequest(serverEntryAst);
4444

4545
if (useInstrumentationAPI) {
46-
instrumentUnstableInstrumentations(serverEntryAst);
46+
instrumentInstrumentations(serverEntryAst);
4747
}
4848

4949
await writeFile(serverEntryAst.$ast, serverEntryPath);
5050
}
5151

52-
function instrumentUnstableInstrumentations(
52+
function instrumentInstrumentations(
5353
originalEntryServerMod: ProxifiedModule<any>,
5454
): void {
5555
const originalEntryServerModAST = originalEntryServerMod.$ast as t.Program;
5656

57-
const hasUnstableInstrumentations = originalEntryServerModAST.body.some(
58-
(node) => {
59-
if (
60-
node.type !== 'ExportNamedDeclaration' ||
61-
node.declaration?.type !== 'VariableDeclaration'
62-
) {
63-
return false;
64-
}
65-
66-
const declarations = node.declaration.declarations;
67-
if (!declarations || declarations.length === 0) {
68-
return false;
69-
}
57+
const hasInstrumentations = originalEntryServerModAST.body.some((node) => {
58+
if (
59+
node.type !== 'ExportNamedDeclaration' ||
60+
node.declaration?.type !== 'VariableDeclaration'
61+
) {
62+
return false;
63+
}
7064

71-
const firstDeclaration = declarations[0];
72-
if (!firstDeclaration || firstDeclaration.type !== 'VariableDeclarator') {
73-
return false;
74-
}
65+
const declarations = node.declaration.declarations;
66+
if (!declarations || declarations.length === 0) {
67+
return false;
68+
}
7569

76-
const id = firstDeclaration.id;
77-
return (
78-
id &&
79-
id.type === 'Identifier' &&
80-
id.name === 'unstable_instrumentations'
81-
);
82-
},
83-
);
70+
const firstDeclaration = declarations[0];
71+
if (!firstDeclaration || firstDeclaration.type !== 'VariableDeclarator') {
72+
return false;
73+
}
8474

85-
if (hasUnstableInstrumentations) {
86-
debug(
87-
'unstable_instrumentations export already exists, skipping adding it again',
75+
const id = firstDeclaration.id;
76+
return (
77+
id &&
78+
id.type === 'Identifier' &&
79+
(id.name === 'instrumentations' ||
80+
id.name === 'unstable_instrumentations')
8881
);
82+
});
83+
84+
if (hasInstrumentations) {
85+
debug('instrumentations export already exists, skipping adding it again');
8986
return;
9087
}
9188

9289
const instrumentationsExport = recast.parse(
93-
`export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()];`,
90+
`export const instrumentations = [Sentry.createSentryServerInstrumentation()];`,
9491
).program.body[0];
9592

9693
originalEntryServerModAST.body.push(instrumentationsExport);

src/react-router/react-router-wizard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ Please create your entry files manually using React Router v7 commands.`);
190190
const detectedVersion = getReactRouterVersion(packageJson) ?? 'unknown';
191191
clack.log.warn(
192192
`The Instrumentation API requires React Router ${chalk.cyan(
193-
'>=7.9.5',
193+
'>=7.15.0',
194194
)} (detected ${chalk.cyan(
195195
detectedVersion,
196196
)}). Your version does not meet this requirement.\n` +

src/react-router/sdk-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export function supportsInstrumentationAPI(
202202
return false;
203203
}
204204

205-
return gte(minVer, '7.9.5');
205+
return gte(minVer, '7.15.0');
206206
}
207207

208208
export async function initializeSentryOnEntryClient(

src/react-router/templates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ startTransition(() => {
139139
${plus(
140140
`<HydratedRouter${
141141
useOnError ? ' onError={Sentry.sentryOnError}' : ''
142-
} unstable_instrumentations={[tracing.clientInstrumentation]} />`,
142+
} instrumentations={[tracing.clientInstrumentation]} />`,
143143
)}
144144
</StrictMode>
145145
);
@@ -230,7 +230,7 @@ ${plus(`export const handleError = Sentry.createSentryHandleError({
230230
});`)}
231231
232232
${plus(`// Enable automatic server-side instrumentation for loaders, actions, middleware
233-
export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()];`)}
233+
export const instrumentations = [Sentry.createSentryServerInstrumentation()];`)}
234234
235235
// ... rest of your server entry`),
236236
);

test/react-router/codemods/client-entry.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ describe('instrumentClientEntry', () => {
294294
);
295295
expect(modifiedContent).toContain('integrations: [tracing]');
296296
expect(modifiedContent).toContain(
297-
'unstable_instrumentations={[tracing.clientInstrumentation]}',
297+
'instrumentations={[tracing.clientInstrumentation]}',
298298
);
299299
});
300300

@@ -317,7 +317,7 @@ describe('instrumentClientEntry', () => {
317317
'integrations: [tracing, Sentry.replayIntegration()]',
318318
);
319319
expect(modifiedContent).toContain(
320-
'unstable_instrumentations={[tracing.clientInstrumentation]}',
320+
'instrumentations={[tracing.clientInstrumentation]}',
321321
);
322322
});
323323

@@ -346,7 +346,7 @@ describe('instrumentClientEntry', () => {
346346
expect(modifiedContent).toContain(
347347
'Sentry.reactRouterTracingIntegration()',
348348
);
349-
expect(modifiedContent).not.toContain('unstable_instrumentations');
349+
expect(modifiedContent).not.toContain('instrumentations');
350350
});
351351
});
352352
});

test/react-router/codemods/server-entry.test.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ describe('Instrumentation API', () => {
610610
}
611611
});
612612

613-
it('should add unstable_instrumentations export when useInstrumentationAPI is true', async () => {
613+
it('should add instrumentations export when useInstrumentationAPI is true', async () => {
614614
const basicContent = fs.readFileSync(
615615
path.join(fixturesDir, 'basic.tsx'),
616616
'utf8',
@@ -626,11 +626,11 @@ describe('Instrumentation API', () => {
626626
'import * as Sentry from "@sentry/react-router";',
627627
);
628628
expect(modifiedContent).toContain(
629-
'export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()];',
629+
'export const instrumentations = [Sentry.createSentryServerInstrumentation()];',
630630
);
631631
});
632632

633-
it('should not add unstable_instrumentations export when useInstrumentationAPI is false', async () => {
633+
it('should not add instrumentations export when useInstrumentationAPI is false', async () => {
634634
const basicContent = fs.readFileSync(
635635
path.join(fixturesDir, 'basic.tsx'),
636636
'utf8',
@@ -642,18 +642,18 @@ describe('Instrumentation API', () => {
642642

643643
const modifiedContent = fs.readFileSync(tmpFile, 'utf8');
644644

645-
expect(modifiedContent).not.toContain('unstable_instrumentations');
645+
expect(modifiedContent).not.toContain('instrumentations');
646646
expect(modifiedContent).not.toContain('createSentryServerInstrumentation');
647647
});
648648

649-
it('should not duplicate unstable_instrumentations export if already present', async () => {
649+
it('should not duplicate instrumentations export if already present', async () => {
650650
const contentWithInstrumentations = `
651651
import { ServerRouter } from 'react-router';
652652
import * as Sentry from '@sentry/react-router';
653653
654654
export default function handleRequest() {}
655655
export const handleError = () => {};
656-
export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()];
656+
export const instrumentations = [Sentry.createSentryServerInstrumentation()];
657657
`;
658658

659659
fs.writeFileSync(tmpFile, contentWithInstrumentations);
@@ -662,8 +662,26 @@ export const unstable_instrumentations = [Sentry.createSentryServerInstrumentati
662662

663663
const modifiedContent = fs.readFileSync(tmpFile, 'utf8');
664664

665-
const count = (modifiedContent.match(/unstable_instrumentations/g) || [])
666-
.length;
665+
const count = (modifiedContent.match(/\binstrumentations\b/g) || []).length;
667666
expect(count).toBe(1);
668667
});
668+
669+
it('should not duplicate if legacy unstable_instrumentations export is already present', async () => {
670+
const contentWithLegacy = `
671+
import { ServerRouter } from 'react-router';
672+
import * as Sentry from '@sentry/react-router';
673+
674+
export default function handleRequest() {}
675+
export const handleError = () => {};
676+
export const unstable_instrumentations = [Sentry.createSentryServerInstrumentation()];
677+
`;
678+
679+
fs.writeFileSync(tmpFile, contentWithLegacy);
680+
681+
await instrumentServerEntry(tmpFile, true);
682+
683+
const modifiedContent = fs.readFileSync(tmpFile, 'utf8');
684+
685+
expect(modifiedContent).not.toContain('export const instrumentations =');
686+
});
669687
});

test/react-router/sdk-setup.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,20 +181,20 @@ describe('React Router SDK Setup', () => {
181181
});
182182

183183
describe('supportsInstrumentationAPI', () => {
184-
it('should return true for React Router v7.9.5 or higher', () => {
184+
it('should return true for React Router v7.15.0 or higher', () => {
185185
expect(
186186
supportsInstrumentationAPI({
187-
dependencies: { '@react-router/dev': '7.9.5' },
187+
dependencies: { '@react-router/dev': '7.15.0' },
188188
}),
189189
).toBe(true);
190190
expect(
191191
supportsInstrumentationAPI({
192-
dependencies: { '@react-router/dev': '^7.9.5' },
192+
dependencies: { '@react-router/dev': '^7.15.0' },
193193
}),
194194
).toBe(true);
195195
expect(
196196
supportsInstrumentationAPI({
197-
dependencies: { '@react-router/dev': '7.10.0' },
197+
dependencies: { '@react-router/dev': '7.16.0' },
198198
}),
199199
).toBe(true);
200200
expect(
@@ -204,15 +204,15 @@ describe('React Router SDK Setup', () => {
204204
).toBe(true);
205205
expect(
206206
supportsInstrumentationAPI({
207-
devDependencies: { '@react-router/dev': '7.9.5' },
207+
devDependencies: { '@react-router/dev': '7.15.0' },
208208
}),
209209
).toBe(true);
210210
});
211211

212-
it('should return false for React Router versions below v7.9.5', () => {
212+
it('should return false for React Router versions below v7.15.0', () => {
213213
expect(
214214
supportsInstrumentationAPI({
215-
dependencies: { '@react-router/dev': '7.9.4' },
215+
dependencies: { '@react-router/dev': '7.14.0' },
216216
}),
217217
).toBe(false);
218218
expect(
@@ -222,7 +222,7 @@ describe('React Router SDK Setup', () => {
222222
).toBe(false);
223223
expect(
224224
supportsInstrumentationAPI({
225-
dependencies: { '@react-router/dev': '7.9.0' },
225+
dependencies: { '@react-router/dev': '7.9.5' },
226226
}),
227227
).toBe(false);
228228
});
@@ -239,12 +239,12 @@ describe('React Router SDK Setup', () => {
239239
it('should handle semver range specifiers correctly', () => {
240240
expect(
241241
supportsInstrumentationAPI({
242-
dependencies: { '@react-router/dev': '~7.9.5' },
242+
dependencies: { '@react-router/dev': '~7.15.0' },
243243
}),
244244
).toBe(true);
245245
expect(
246246
supportsInstrumentationAPI({
247-
dependencies: { '@react-router/dev': '>=7.9.5' },
247+
dependencies: { '@react-router/dev': '>=7.15.0' },
248248
}),
249249
).toBe(true);
250250
});

0 commit comments

Comments
 (0)