Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- feat(react-router): Use `sentryOnError` on `HydratedRouter` instead of mutating `root.tsx` ErrorBoundary

## 6.12.0

### Features
Expand Down
9 changes: 3 additions & 6 deletions e2e-tests/tests/react-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,9 @@ describe('React Router', () => {
]);
});

test('root file contains Sentry ErrorBoundary', () => {
checkFileContents(`${projectDir}/app/root.tsx`, [
'import * as Sentry from',
'@sentry/react-router',
'export function ErrorBoundary',
'Sentry.captureException(error)',
test('entry.client file contains onError prop on HydratedRouter', () => {
checkFileContents(`${projectDir}/app/entry.client.tsx`, [
'onError={Sentry.sentryOnError}',
]);
});

Expand Down
167 changes: 96 additions & 71 deletions src/react-router/codemods/client.entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */

import * as recast from 'recast';
import * as path from 'path';
import type { namedTypes as t } from 'ast-types';
import type { ExpressionKind } from 'ast-types/lib/gen/kinds';

// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
Expand All @@ -24,27 +24,24 @@ export async function instrumentClientEntry(
): Promise<void> {
const clientEntryAst = await loadFile(clientEntryPath);

if (hasSentryContent(clientEntryAst.$ast as t.Program)) {
const filename = path.basename(clientEntryPath);
clack.log.info(`Sentry initialization found in ${chalk.cyan(filename)}`);
return;
}
const alreadyHasSentry = hasSentryContent(clientEntryAst.$ast as t.Program);

clientEntryAst.imports.$add({
from: '@sentry/react-router',
imported: '*',
local: 'Sentry',
});
if (!alreadyHasSentry) {
clientEntryAst.imports.$add({
from: '@sentry/react-router',
imported: '*',
local: 'Sentry',
});

let initContent: string;
let initContent: string;

if (useInstrumentationAPI && enableTracing) {
const integrations = ['tracing'];
if (enableReplay) {
integrations.push('Sentry.replayIntegration()');
}
if (useInstrumentationAPI && enableTracing) {
const integrations = ['tracing'];
if (enableReplay) {
integrations.push('Sentry.replayIntegration()');
}

initContent = `
initContent = `
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
Expand All @@ -59,63 +56,74 @@ Sentry.init({
: ''
}
});`;
} else {
const integrations = [];
if (enableTracing) {
integrations.push('Sentry.reactRouterTracingIntegration()');
}
if (enableReplay) {
integrations.push('Sentry.replayIntegration()');
}
} else {
const integrations = [];
if (enableTracing) {
integrations.push('Sentry.reactRouterTracingIntegration()');
}
if (enableReplay) {
integrations.push('Sentry.replayIntegration()');
}

initContent = `
initContent = `
Sentry.init({
dsn: "${dsn}",
sendDefaultPii: true,
integrations: [${integrations.join(', ')}],
${enableLogs ? 'enableLogs: true,' : ''}
tracesSampleRate: ${enableTracing ? '1.0' : '0'},${
enableTracing
? '\n tracePropagationTargets: [/^\\//, /^https:\\/\\/yourserver\\.io\\/api/],'
: ''
}${
enableReplay
? '\n replaysSessionSampleRate: 0.1,\n replaysOnErrorSampleRate: 1.0,'
: ''
}
enableTracing
? '\n tracePropagationTargets: [/^\\//, /^https:\\/\\/yourserver\\.io\\/api/],'
: ''
}${
enableReplay
? '\n replaysSessionSampleRate: 0.1,\n replaysOnErrorSampleRate: 1.0,'
: ''
}
});`;
}

(clientEntryAst.$ast as t.Program).body.splice(
getAfterImportsInsertionIndex(clientEntryAst.$ast as t.Program),
0,
...recast.parse(initContent).program.body,
);
}

(clientEntryAst.$ast as t.Program).body.splice(
getAfterImportsInsertionIndex(clientEntryAst.$ast as t.Program),
0,
...recast.parse(initContent).program.body,
const useInstrAPI = useInstrumentationAPI && enableTracing;

if (useInstrAPI && !alreadyHasSentry) {
addInstrumentationPropsToHydratedRouter(clientEntryAst.$ast as t.Program);
}

const hydratedRouterFound = addOnErrorToHydratedRouter(
clientEntryAst.$ast as t.Program,
);

if (useInstrumentationAPI && enableTracing) {
const hydratedRouterFound = addInstrumentationPropsToHydratedRouter(
clientEntryAst.$ast as t.Program,
if (!hydratedRouterFound) {
const instrSnippet =
useInstrAPI && !alreadyHasSentry
? ' unstable_instrumentations={[tracing.clientInstrumentation]}'
: '';
clack.log.warn(
`Could not find ${chalk.cyan(
'HydratedRouter',
)} component in your client entry file.\n` +
`Manually add the following props:\n` +
` ${chalk.green(
`<HydratedRouter onError={Sentry.sentryOnError}${instrSnippet} />`,
)}`,
);

if (!hydratedRouterFound) {
clack.log.warn(
`Could not find ${chalk.cyan(
'HydratedRouter',
)} component in your client entry file.\n` +
`To use the Instrumentation API, manually add the ${chalk.cyan(
'unstable_instrumentations',
)} prop:\n` +
` ${chalk.green(
'<HydratedRouter unstable_instrumentations={[tracing.clientInstrumentation]} />',
)}`,
);
}
}

await writeFile(clientEntryAst.$ast, clientEntryPath);
}

function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {
function addPropToHydratedRouter(
ast: t.Program,
propName: string,
propValue: ExpressionKind,
): boolean {
let found = false;

recast.visit(ast, {
Expand All @@ -128,30 +136,23 @@ function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {
) {
found = true;

const hasInstrumentationsProp = openingElement.attributes?.some(
const hasProp = openingElement.attributes?.some(
(attr) =>
attr.type === 'JSXAttribute' &&
attr.name.type === 'JSXIdentifier' &&
attr.name.name === 'unstable_instrumentations',
attr.name.name === propName,
);

if (!hasInstrumentationsProp) {
const instrumentationsProp = recast.types.builders.jsxAttribute(
recast.types.builders.jsxIdentifier('unstable_instrumentations'),
recast.types.builders.jsxExpressionContainer(
recast.types.builders.arrayExpression([
recast.types.builders.memberExpression(
recast.types.builders.identifier('tracing'),
recast.types.builders.identifier('clientInstrumentation'),
),
]),
),
if (!hasProp) {
const prop = recast.types.builders.jsxAttribute(
recast.types.builders.jsxIdentifier(propName),
recast.types.builders.jsxExpressionContainer(propValue),
);

if (!openingElement.attributes) {
openingElement.attributes = [];
}
openingElement.attributes.push(instrumentationsProp);
openingElement.attributes.push(prop);
}

return false;
Expand All @@ -163,3 +164,27 @@ function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {

return found;
}

function addOnErrorToHydratedRouter(ast: t.Program): boolean {
return addPropToHydratedRouter(
ast,
'onError',
recast.types.builders.memberExpression(
recast.types.builders.identifier('Sentry'),
recast.types.builders.identifier('sentryOnError'),
),
);
}

function addInstrumentationPropsToHydratedRouter(ast: t.Program): boolean {
return addPropToHydratedRouter(
ast,
'unstable_instrumentations',
recast.types.builders.arrayExpression([
recast.types.builders.memberExpression(
recast.types.builders.identifier('tracing'),
recast.types.builders.identifier('clientInstrumentation'),
),
]),
);
}
Loading
Loading