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

feat(react): Add TanStack Router integration #12095

Merged
merged 10 commits into from
May 29, 2024
Merged

Conversation

MicahLyle
Copy link
Contributor

@MicahLyle MicahLyle commented May 17, 2024

Before submitting a pull request, please take a look at our
Contributing guidelines and verify:

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).

Example Repo I Worked Off Of: https://github.com/MicahLyle/tanstack-router-sentry-exploration

Potential Notes/TODOs/Things to Address

  • Thank you @lforst for all the help.
  • I based this mostly off of the Next JS Client Router implementation. It was relatively straightforward because TanStack Router has pretty nice observability with subscribe(...).
  • It's not clear if const matches = state.pendingMatches ?? state.matches; is the right approach, or if I should bail immediately if pendingMatches is not present. @tannerlinsley any input here?
  • I took the if (instrumentPageLoad && initPathName) code from the React Router v6 tracing code. It seemed like something we'd want for an SPA: There's only a page load on the initial load and then from there we have only navigation instrumentation. Correct me if I'm wrong though.
  • @lforst - Handing off tests and docs to you 😅. If we can get an "experimental" release out, I'm happy to help test it in the wild as I have some projects I'll be able to migrate to Tanstack Router in the near future once this is merged.
  • I "vendored" the types best I could per @lforst's suggestion.
  • For future improvements, TanStack Router has a lot of other information exposed in the state. I kind of hinted at that by adding some additional type properties that are not currently used, but may end up being useful as this integration is expanded.
  • I'm just letting Sentry's default mechanics detect when the navigation is done, but I'm presuming we can get that info from TanStack Router itself, so that would probably be a future direction to head in.
  • There's a number of edge cases and things I didn't check or thoroughly look into, but, again, happy to help test this in the wild once an initial version is merged.

@lforst
Copy link
Member

lforst commented May 21, 2024

Thanks again for doing this! I'll need some time to catch up on things with our v8 release but I'll make sure that this makes it in!

const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname;
if (instrumentPageLoad && initPathName) {
startBrowserTracingPageLoadSpan(client, {
name: initPathName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there no way for us to get the routeId for the pageload? We can read this from router.state also right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I believe so. Sorry, I missed putting it there. @lforst can you put that in?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Route ID is on each match. You'll probably want the deepest one: matches[matches.length - 1].routeId
  • Entire route config is available at: router.routesById[routeId]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did the matches[matches.length - 1].routeId approach below, just forgot to put it here as well.

@ruiaraujo012
Copy link

ruiaraujo012 commented May 23, 2024

Hi @MicahLyle, this thread on Tanstack discord could help you here https://discord.com/channels/719702312431386674/1240064824021483570

export function tanstackRouterInstrumentNavigation(router: TanstackRouter, client: Client): void {
router.history.subscribe(() => {
const state = router.state;
const matches = state.pendingMatches ?? state.matches;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like what we do internally is more like this:

const matches = [
  ...state.cachedMatches,
  ...(state.pendingMatches ?? []),
  ...state.matches,
]

see: https://github.com/TanStack/router/blob/bce8e71652acdfa66594fedaab4bc5f058ccaf6e/packages/react-router/src/RouterProvider.tsx#L113-L117

I did find multiple implementations of this with different spreading order, so I'm not sure which one would be correct 😅

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want one of these depending on the situation:

  • state.pendingMatches || state.matches - The user may be in the process of navigating to a new location, which is why you would want pendingMatches to represent that pending navigation
  • state.matches - If you're only interested in where the user is currently and are not interested in any potential pending navigation destinations, then this should suffice.

I recommend the former. Do not use state.cachedMatches for anything outside of reporting back stuff that's cached.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tannerlinsley I did const matches = state.pendingMatches ?? state.matches;. Would that be worse than state.pendingMatches || state.matches?

}

export function tanstackRouterInstrumentNavigation(router: TanstackRouter, client: Client): void {
router.history.subscribe(() => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about the scope, but do we not need to unsubscribe at one point?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can add one, we just need to hook it onto closing the Sentry client, which we currently don't have an exposed hook for in the public API.

@lforst lforst self-assigned this May 27, 2024
@lforst lforst changed the title Initial TanStack Router Integration feat(react): Add TanStack Router integration May 27, 2024
@lforst
Copy link
Member

lforst commented May 28, 2024

Ok, I got this into a minimal working state and added some tests. Not 100% sure on the implementation but I think it should work correctly.

One problem I was facing is that we couldn't just straight up use the router.history.subscribe() hook to trigger navigations because at the time it fires, the state is stale and pendingMatches is still undefined. The things we need (ie. the new parameterized route) aren't inside the state yet so what we do is we take the pathname as the default navigation name and update it with proper values as soon as the onResolved hook fires. Since the onResolved hook doesn't give us the parameterized name, we throw the resolved pathname and search values into router.matchRoutes() to get the parameterized route.

I also dumped the route params onto the span as attributes.

@AbhiPrasad would appreciate another pass!


// The following types are vendored types from TanStack Router, so we don't have to depend on the actual package

export interface VendoredTanstackRouter {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move this into a separate file and copy over license as well, I think we need to give attribution.


const paramAttributes: Record<string, string> = {};
for (const key of Object.keys(match.params)) {
paramAttributes[`params.${key}`] = match.params[key];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we only set this with sendDefaultPii?

Also, I think we should call this url.path.params.X because the OTEL attribute for path is url.path.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we only set this with sendDefaultPii?

I would opt against it since all of the params are inside the URL (which we send by default) anyhow.

If for some reason, we would be sending additional data after a patch, I would guard it behind sendDefaultPii.

Also, I think we should call this url.path.params.X because the OTEL attribute for path is url.path

👍

"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"@playwright/test": "^1.41.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@playwright/test": "^1.41.1"
"@playwright/test": "^1.41.1",
"@sentry-internal/event-proxy-server": "link:../../../event-proxy-server"

I think this is missing here...?

Copy link
Member

@lforst lforst May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's odd but it doesn't seem to matter. I think because the package is available in the root node_modules. Still gonna add it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I think that's why it works, we should still add it for safety/correctness I guess :D

@lforst lforst merged commit 0d1093d into getsentry:develop May 29, 2024
66 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants