Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react-router/cloudflare';
import { type LoaderFunctionArgs } from '@shopify/remix-oxygen';
import {
Outlet,
Expand Down Expand Up @@ -160,8 +159,6 @@ export function ErrorBoundary({ error }: { error: unknown }) {
let errorMessage = 'Unknown error';
let errorStatus = 500;

const eventId = Sentry.captureException(error);

if (isRouteErrorResponse(error)) {
errorMessage = error?.data?.message ?? error.data;
errorStatus = error.status;
Expand All @@ -178,11 +175,6 @@ export function ErrorBoundary({ error }: { error: unknown }) {
<pre>{errorMessage}</pre>
</fieldset>
)}
{eventId && (
<h2>
Sentry Event ID: <code>{eventId}</code>
</h2>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react-router';
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
Expand Down Expand Up @@ -48,7 +47,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);
if (import.meta.env.DEV) {
details = error.message;
stack = error.stack;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand All @@ -127,7 +128,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ startTransition(() => {
document,
<StrictMode>
{/* unstable_instrumentations is React Router 7.x's prop name (will become `instrumentations` in v8) */}
<HydratedRouter unstable_instrumentations={sentryClientInstrumentation} />
<HydratedRouter unstable_instrumentations={sentryClientInstrumentation} onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react-router';
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
Expand Down Expand Up @@ -48,7 +47,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);
if (import.meta.env.DEV) {
details = error.message;
stack = error.stack;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react-router';
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
Expand Down Expand Up @@ -48,7 +47,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);
if (import.meta.env.DEV) {
details = error.message;
stack = error.stack;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand All @@ -127,7 +128,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration }

import type { Route } from './+types/root';
import './app.css';
import * as Sentry from '@sentry/react-router';

export function Layout({ children }: { children: React.ReactNode }) {
return (
Expand Down Expand Up @@ -35,8 +34,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);

details = error.message;
stack = error.stack;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand All @@ -127,7 +128,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration }

import type { Route } from './+types/root';
import './app.css';
import * as Sentry from '@sentry/react-router';

export function Layout({ children }: { children: React.ReactNode }) {
return (
Expand Down Expand Up @@ -35,8 +34,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);

details = error.message;
stack = error.stack;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand All @@ -127,7 +128,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
<HydratedRouter onError={Sentry.sentryOnError} />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/react-router';
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
Expand Down Expand Up @@ -48,7 +47,6 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
message = error.status === 404 ? '404' : 'Error';
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
} else if (error && error instanceof Error) {
Sentry.captureException(error);
if (import.meta.env.DEV) {
details = error.message;
stack = error.stack;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand All @@ -127,7 +128,8 @@ test.describe('client-side errors', () => {
type: 'Error',
value: errorMessage,
mechanism: {
handled: true,
handled: false,
type: 'auto.function.react_router.on_error',
},
},
],
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export {
isNavigateHookInvoked,
type CreateSentryClientInstrumentationOptions,
} from './createClientInstrumentation';

export { sentryOnError } from './sentryOnError';
36 changes: 36 additions & 0 deletions packages/react-router/src/client/sentryOnError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { captureException } from '@sentry/core';
import { captureReactException } from '@sentry/react';

/**
* A handler function for React Router's `onError` prop on `HydratedRouter`.
*
* Reports errors to Sentry.
*
* @example (entry.client.tsx)
* ```tsx
* import { sentryOnError } from '@sentry/react-router';
*
* startTransition(() => {
* hydrateRoot(
* document,
* <HydratedRouter onError={sentryOnError} />
* );
* });
* ```
*/
export function sentryOnError(
error: unknown,
{
errorInfo,
}: {
errorInfo?: React.ErrorInfo;
},
): void {
const mechanism = { handled: false, type: 'auto.function.react_router.on_error' };

if (errorInfo) {
captureReactException(error, errorInfo, { mechanism });
} else {
captureException(error, { mechanism });
}
}
38 changes: 38 additions & 0 deletions packages/react-router/test/client/sentryOnError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as SentryCore from '@sentry/core';
import * as SentryReact from '@sentry/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { sentryOnError } from '../../src/client/sentryOnError';

const captureReactExceptionSpy = vi.spyOn(SentryReact, 'captureReactException').mockReturnValue('mock-event-id');
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException').mockReturnValue('mock-event-id');

describe('sentryOnError', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('calls captureReactException when errorInfo is provided', () => {
const error = new Error('test error');
const errorInfo = { componentStack: '<TestComponent>\n<App>' };

sentryOnError(error, {
errorInfo,
});

expect(captureReactExceptionSpy).toHaveBeenCalledWith(error, errorInfo, {
mechanism: { handled: false, type: 'auto.function.react_router.on_error' },
});
expect(captureExceptionSpy).not.toHaveBeenCalled();
});

it('calls captureException when errorInfo is undefined', () => {
const error = new Error('loader error');

sentryOnError(error, {});

expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
mechanism: { handled: false, type: 'auto.function.react_router.on_error' },
});
expect(captureReactExceptionSpy).not.toHaveBeenCalled();
});
});
Loading