Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support React Router v6 #4118

Closed
quisido opened this issue Nov 4, 2021 · 31 comments
Closed

Support React Router v6 #4118

quisido opened this issue Nov 4, 2021 · 31 comments

Comments

@quisido
Copy link
Contributor

quisido commented Nov 4, 2021

The reactRouterV#Instrumentation is no longer supported in React Router v6. Unlike previous versions of React Router, v6 does not expose the history object.

I've attempted to solve this personally, so as to unblock myself in my projects: react-router-v6-instrumentation on NPM.

Example usage:

import { init } from '@sentry/react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import useBrowserTracing from 'react-router-v6-instrumentation';

function App() {
  const browserTracing = useBrowserTracing();

  // Initialize Sentry with the browser tracing integration.
  useEffect(() => {
    init({
      integrations: [browserTracing],
    });
  }, [browserTracing]);

  return <>Hello world!</>;
}

render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root'),
);

Source code on GitHub

I hope this ticket inspires the maintainers to implement (credit? 😆) or unblocks others who are searching for solutions to this issue.

@AbhiPrasad
Copy link
Member

Awesome work man, the approach looks good from an initial glance.

If you want to, feel free to throw up a PR to add this our @sentry/react package, we can help with writing tests and adding documentation. Otherwise, we can also look at porting this over (giving full credit to you of course), but that might take a bit.

@amccloud
Copy link

amccloud commented Nov 8, 2021

@CharlesStover anyway to use the integration when calling Sentry.init before render?

@quisido
Copy link
Contributor Author

quisido commented Nov 8, 2021

@AbhiPrasad just for notes sake, in case y'all tackle this before I do:

  • I would replace useBrowserTracing with useRoutingInstrumentation and have the consumer call new BrowserTracing({ routingInstrumentation }) themselves.
  • The pathname mutable object reference exists solely so that location does not have to be passed to the memoized BrowserTracing object (or routingInstrumentation function). I didn't check if location is a constant reference even when pathname, hash, and search change. If location is constant across route changes, then you can remove the entire location reference and effect hook that update it; instead, you can just pass location to the dependency array directly and not worry about the memoized value ever changing reference. This is probably a simple test and sorry I didn't do it, but I'd recommend it for an official implementation.

@amccloud Unfortunately, as far as I'm aware, you cannot integrate with React Router v6 before render. Since React Router v6 does not let you pass the history object on mount (before: <Router history={...}>; after: <Router>), there is no way to attach event listeners before React Router mounts. 🤷 It's honestly probably for the best for React Router to keep history abstracted away, despite it making integration more difficult. If calling init on render is annoying to maintain, you can use the sentry-react package which does just that: pass the init parameters as props, and it will call init on mount for you.

@quisido
Copy link
Contributor Author

quisido commented Nov 11, 2021

As a further progress report, I have:

  1. Released a 2.0.0 version of the package that replaces useBrowserTracing with useRoutingInstrumentation, aligning it closer to the original reactRouterV#Instrumentation implementations and allowing consumers to configuring their BrowserTracing instance.
  2. Confirmed that location does change reference, therefore the use of a MutableObjectRef for the location pathname is necessary.

The code base should therefore hypothetically be ready to merge into @sentry/react and accessible to all consumers. I may still PR this depending on time constraints, but you also still welcome to pursue it. 🙂

EDIT: What does "credit" look like for a contribution like this? Is there concern with simply using export { default as useReactRouterV6Instrumentation } from 'react-router-v6-instrumentation'; as a dependency? That would be gratifying for my download/usage tracker. 😅 Otherwise, I assume credit looks like a source code comment with a link to the original repository?

@AbhiPrasad
Copy link
Member

AbhiPrasad commented Nov 11, 2021

I’m planning to take a look at adding this next week, but feel free to jump on it!

As per https://develop.sentry.dev/sdk/philosophy/#dependencies-cost, we shy away from external deps as much as possible. This is also useful from a compliance and security perspective for our bigger users. This means we probably will port over the instrumentation you built (and adjust it for the sdk constraints + add tests).

Credit here means we will leave a code comment to your GH repo + your name. We did something similar with Python and ASGI: https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/asgi.py#L4

@quisido
Copy link
Contributor Author

quisido commented Nov 11, 2021

Awesome and makes sense. It's already 100% tested. With that example for Python, I should be able to PR this change, assuming I can integrate with your test runner intuitively.

@AbhiPrasad
Copy link
Member

Hey @CharlesStover, how does your hook deal with parameterized URLs? (Like path/to/:id?).

We have to make sure we report the parameterized URLs, not the actual one, so that transactions can be grouped properly.

@quisido
Copy link
Contributor Author

quisido commented Nov 13, 2021

Thanks for the follow-up.

My hook's implementation is a copy of the reactRouterV#Instrumentation functions vended by Sentry today, when the only parameter is history, i.e. reactRouterV5Instrumentation(history). The behavior that comes with passing RouteConfig[] and MatchPath is not included.

@AbhiPrasad
Copy link
Member

Yeah I wonder if there’s an easy way for us to just get the paramterized URL without recomputing the match or requiring excess things in user config.

I’m going to try something out based on your hook, but if not will also try opening an issue with the react router folks to see if they have any ideas.

Appreciate your help with this @CharlesStover!

@AbhiPrasad
Copy link
Member

Alright made a discussion on remix-run/react-router#8327, let's see what happens.

@marklai1998
Copy link

Any progress on this?

@AbhiPrasad
Copy link
Member

Hey @marklai1998, currently we are waiting for a response from the remix folks (in the discussion linked above), to see if they can help us out. If that doesn't go anywhere, I guess we'll just have to figure it out ourselves - so no ETA atm.

@quisido
Copy link
Contributor Author

quisido commented Nov 17, 2021

FWIW, the package mentioned in the OP can unblock anyone in the meantime. 😅 @marklai1998

@h3rmanj
Copy link
Contributor

h3rmanj commented Dec 4, 2021

FYI, react-router just merged a HistoryRouter, where you can pass your own history object. This will help with being able to initialize Sentry before rendering at least.

remix-run/react-router#7586

@quisido
Copy link
Contributor Author

quisido commented Dec 7, 2021

@CharlesStover using your library we can aggregate the url's that using id? for example, instead show in sentry /container/123/homepage aggregate and show /container/:userId/homepage

Unfortunately no, I do not support that feature. There is an open issue with React Router to expose the unparsed path.

@lukemorales
Copy link

FYI, react-router just merged a HistoryRouter, where you can pass your own history object. This will help with being able to initialize Sentry before rendering at least.

remix-run/react-router#7586

Since we already have a HistoryRouter in React Router v6, when is Sentry.reactRouterV6Instrumentation going to be added to the @sentry/react package?

@h3rmanj
Copy link
Contributor

h3rmanj commented Jan 4, 2022

Since we already have a HistoryRouter in React Router v6, when is Sentry.reactRouterV6Instrumentation going to be added to the @sentry/react package?

I might be wrong here, but since Sentry.reactRouterV5Instrumentation accepts a history object, and both React Router v5 and v6 depends on history v5, I'd guess it already works with React Router v6.
Edit: I was wrong

@lukemorales
Copy link

Since we already have a HistoryRouter in React Router v6, when is Sentry.reactRouterV6Instrumentation going to be added to the @sentry/react package?

I might be wrong here, but since Sentry.reactRouterV5Instrumentation accepts a history object, and both React Router v5 and v6 depends on history v5, I'd guess it already works with React Router v6.

Typescript is not happy when passing History v5 to Sentry.reactRouterV5Instrumentation as it has a different type declaration, I'm not sure if it's just a matter of adding the History V5 type to the Sentry.reactRouterV5Instrumentation params or if something core changed to how History v5 behaves differently from History v4

@h3rmanj
Copy link
Contributor

h3rmanj commented Jan 4, 2022

I was wrong, React Router v5 depends on history v4.

I'm guessing they are waiting on input from the React Router team (remix-run/react-router#8327), before integrating a v6 instrumentation, but they are hard to reach. Maybe someone should ping them?

@xyy94813
Copy link

Can i intergrate react-router without integration when init???

like this:

function APP () {
 const location = useLocation();
 useEffect(() => {
   getCurrentHub().withScope(() => {
    // do something
   })
 }, [location])
}

Then we can init sentry before render.

@legraphista
Copy link

legraphista commented Feb 22, 2022

FWIW, I created a copy of the V5 history object and monkey patched the listener as that appears to be the only incompatibility (according to typescript defs, I didn't go deeper into code to see if that's the only difference 🤷 )

history.ts

import {createHashHistory} from "history";
import {Action, Location} from "@sentry/react/dist/types";

export const hashHistory = createHashHistory({ window })

export const hashHistoryV4Compat = {
  ...hashHistory,
  listen: (cb: (location: Location, action: Action) => void) => {
    return hashHistory.listen((update) => {
      cb(update.location, update.action);
    })
  }
}

sentry.ts

import {hashHistoryV4Compat} from "./history";
import * as Sentry from '@sentry/react'

Sentry.init({
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.reactRouterV5Instrumentation(hashHistoryV4Compat),
      }),
    ]
})

app.tsx

import "./sentry";

import {unstable_HistoryRouter as Router} from 'react-router-dom'
import {hashHistory} from "./history";

....

      <Router history={hashHistory}>

....

From my preliminary tests, it looks like it works just fine

I'm sure that something is probably broken behind the scenes so use this at your own risk 😅, but my Sentry panel gets populated correctly

In case you're wondering why unstable_HistoryRouter is prefixed by unstable_, here are the docs

@razor-x
Copy link

razor-x commented Mar 1, 2022

@legraphista What did you do about using const SentryRoute = Sentry.withSentryRouting(Route);? If I do this with react-router v6 I get the children of <Routes> must be a <Route> error.

@dpvdberg
Copy link

When I use @legraphista's tactic with the react-router-config routes as second parameter (as described in option one of the react router v5 integration guide), like so:
Sentry.reactRouterV5Instrumentation(hashHistoryV4Compat, routes)
it still won't render the routes correctly in Sentry. Any ideas?

@AbhiPrasad AbhiPrasad mentioned this issue Apr 7, 2022
26 tasks
@dcramer
Copy link
Member

dcramer commented Apr 13, 2022

function useSentry() {
  const matches = useMatches();
  useEffect(
    () => {
      Sentry.configureScope((scope) => {
        scope.setTransactionName(matches[matches.length - 1].id);
      });
    },
    matches.map((m) => m.id)
  );
}

Toying around with a POC on Remix and this definitely gives a usable transaction name. I'm not sure theres an event easily hookable, but we could bind this on demand in a few places assuming we can access context.

@wontondon
Copy link

I took a different approach to workaround this issue. I like that it uses the current react router v6 public API and supports the parameterized paths. I used inspiration from some of the components like <Navigate> in react-router and peeking at the v5 sentry implementation. I ended up with a component <SentryReactRouterV6RouterInstrumentation /> which uses the current public react router v6 API. It listens to navigation changes and sends events to sentry. I'm not a React expert, so I'm wouldn't be surprised if there are some issues, but it seems to work. This implementation also has support for the parameter matching /event/123 sent as /event/:id

I have published the source of SentryReactRouterV6RouterInstrumentation and a sample usage in a gist -https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536.

Hope it helps. If it is really useful I can try and get it published.

@smeubank
Copy link
Member

this is shipped in betas for V7! 🚀

we will close this request and would love to get any feedback on the usage!

@AbhiPrasad
Copy link
Member

v7 has been officially released! Please see our full set of docs for using this integration: https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/react-router/#react-router-v6

@FilipaBarroso
Copy link

Is there a way to use this integration with useRoutes, or any plans to support it in the future?

@onurtemizkan
Copy link
Collaborator

Thanks @FilipaBarroso, just opened an issue for useRoutes support.

#5338

@Maksym-Tkachuk
Copy link

The correct way to integrate React Router with Sentry

Right place for create HOC SentryRoutes

Create HOC SentryRoutes out of component body. If you create inside the component and there are react-router-dom`s hooks especially (useLocation and useNavigate) then after build the project Routers state is cleaned up each time the route changes. That will cause all states to be reset in case of nested routes.

Correct implementation

import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from 'react-router-dom';
Sentry.init({
  dsn: '<dsn-link-redacted>',
  integrations: [
    new BrowserTracing({
      routingInstrumentation: Sentry.reactRouterV6Instrumentation(
        React.useEffect,
        useLocation,
        useNavigationType,
        createRoutesFromChildren,
        matchRoutes
      )
    })
  ],
  tracesSampleRate: 1.0
});
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); // TODO place this HOC out of App
const App = () => (
  <BrowserRouter>
    <SentryRoutes>
      <Route element={<div>Edit Page</div>} path="foo/edit" />
      <Route element={<div>View Page</div>} path="foo/barId" />
    </SentryRoutes>
  </BrowserRouter>
);
``` (отре

@matheusgoc
Copy link

I'm wondering if somebody can help with this. I've just followed the directions at Sentry docs to set up Sentry with React and React Router and it's not working. I'm using React v18+ and Sentry v7+ and I only have @sentry/react, @sentry/tracing, and @sentry/cli. Any issue reported to Sentry is displayed like that:

TypeError ?(bundle)
o._mergeOptions is not a function

The stack trace points the error to ./node_modules/@sentry/core/esm/integrations/inboundfilters.js suggesting that the problem is on Sentry.

This is the React entry point code I have:

import ReactDOM from 'react-dom/client';
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
import {
  RouterProvider,
  createBrowserRouter,
  createRoutesFromChildren,
  matchRoutes,
  useLocation,
  useNavigationType,
} from 'react-router-dom';
import './assets/styles/global.css';
import reportWebVitals from './reportWebVitals';
import Home from './pages/home';
import Error from './pages/error';
import MyAppointments from './pages/my-appointments';
import Meetings from './pages/meetings';
import CancelAppointment from './pages/cancel-appointment';
import { useEffect } from 'react';

// basic sentry setup without react-router
// if (process.env.REACT_APP_SENTRY_DSN) {
//   Sentry.init({
//     dsn: process.env.REACT_APP_SENTRY_DSN,
//     integrations: [new BrowserTracing()],
//     environment: process.env.NODE_ENV,
//     tracesSampleRate: parseFloat(process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE || '0.2'),
//     release: process.env.REACT_APP_SENTRY_RELEASE,
//   });
// }

// sentry setup with react-router instrumentation
if (process.env.REACT_APP_SENTRY_DSN) {
  Sentry.init({
    dsn: process.env.REACT_APP_SENTRY_DSN,
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.reactRouterV6Instrumentation(
          useEffect,
          useLocation,
          useNavigationType,
          createRoutesFromChildren,
          matchRoutes,
        ),
      }),
    ],
    tracesSampleRate: parseFloat(process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE || '0.2'),
    environment: process.env.NODE_ENV,
    release: process.env.REACT_APP_SENTRY_RELEASE,
  });
}

const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter);
const router = sentryCreateBrowserRouter([
  {
    path: '/',
    element: <Home />,
    errorElement: <Error />,
  },
  {
    path: '/my_appointments',
    element: <MyAppointments />,
    errorElement: <Error />,
  },
  {
    path: '/meetings',
    element: <Meetings />,
    errorElement: <Error />,
  },
  {
    path: '/cancel_appointment',
    element: <CancelAppointment />,
    errorElement: <Error />,
  },
]);

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <RouterProvider router={router} />,
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

and these are my dependencies:

  "dependencies": {
    "@sentry/react": "^7.34.0",
    "@sentry/tracing": "^7.35.0",
    "axios": "^1.2.3",
    "dayjs": "^1.11.7",
    "react": "^18.2.0",
    "react-countdown": "^2.3.5",
    "react-dom": "^18.2.0",
    "react-router-dom": "6",
    "react-scripts": "5.0.1",
    "simple-crypto-js": "^3.0.1",
    "typescript": "^4.9.4",
    "web-vitals": "^2.1.0"
  },

I add an exception for testing purpose at MyAppointments like this throw new Error('testing sentry error report'); but as explained Sentry always issue the same TypeError. The problem persists Even hardcoding the env vars or using basic set up without react router instrumentation.

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

No branches or pull requests