diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 20ecee5b51..00c2c33266 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -65,7 +65,8 @@ dev,npm-run-all,MIT,Copyright 2015 Toru Nagashima dev,pako,MIT,(C) 2014-2017 Vitaly Puzrin and Andrey Tupitsin dev,prettier,MIT,Copyright James Long and contributors dev,puppeteer,Apache-2.0,Copyright 2017 Google Inc. -dev,react-router-dom,MIT,Copyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023 +dev,react-router-dom-6,MIT,Copyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023 +dev,react-router-dom-7,MIT,Copyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023 dev,style-loader,MIT,Copyright JS Foundation and other contributors dev,terser-webpack-plugin,MIT,Copyright JS Foundation and other contributors dev,ts-loader,MIT,Copyright 2015 TypeStrong diff --git a/eslint-local-rules/disallowSideEffects.js b/eslint-local-rules/disallowSideEffects.js index 21859edb6e..322882493d 100644 --- a/eslint-local-rules/disallowSideEffects.js +++ b/eslint-local-rules/disallowSideEffects.js @@ -37,6 +37,8 @@ const packagesWithoutSideEffect = new Set([ '@datadog/browser-rum-core', 'react', 'react-router-dom', + 'react-router-6', + 'react-router-7', ]) /** diff --git a/packages/core/src/domain/connectivity/connectivity.ts b/packages/core/src/domain/connectivity/connectivity.ts index 529c1df627..4d6551457c 100644 --- a/packages/core/src/domain/connectivity/connectivity.ts +++ b/packages/core/src/domain/connectivity/connectivity.ts @@ -8,6 +8,7 @@ interface BrowserNavigator extends Navigator { export interface NetworkInformation { type?: NetworkInterface effectiveType?: EffectiveType + saveData: boolean } export interface Connectivity { diff --git a/packages/core/test/emulate/mockNavigator.ts b/packages/core/test/emulate/mockNavigator.ts index c9e38bc5f6..6a43c2332b 100644 --- a/packages/core/test/emulate/mockNavigator.ts +++ b/packages/core/test/emulate/mockNavigator.ts @@ -13,7 +13,7 @@ export function setNavigatorOnLine(onLine: boolean) { }) } -export function setNavigatorConnection(connection: NetworkInformation | undefined) { +export function setNavigatorConnection(connection: Partial | undefined) { Object.defineProperty(navigator, 'connection', { get() { return connection diff --git a/packages/rum-react/package.json b/packages/rum-react/package.json index 2b92dc67fc..38455a82fb 100644 --- a/packages/rum-react/package.json +++ b/packages/rum-react/package.json @@ -17,7 +17,7 @@ }, "peerDependencies": { "react": "18", - "react-router-dom": "6" + "react-router-dom": "6 || 7" }, "peerDependenciesMeta": { "@datadog/browser-rum": { @@ -38,7 +38,8 @@ "@types/react-dom": "18.3.5", "react": "18.3.1", "react-dom": "18.3.1", - "react-router-dom": "6.28.2" + "react-router-dom-6": "npm:react-router-dom@6.28.2", + "react-router-dom-7": "npm:react-router-dom@7.1.3" }, "repository": { "type": "git", diff --git a/packages/rum-react/react-router-v7/package.json b/packages/rum-react/react-router-v7/package.json new file mode 100644 index 0000000000..f152762656 --- /dev/null +++ b/packages/rum-react/react-router-v7/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "main": "../cjs/entries/reactRouterV7.js", + "module": "../esm/entries/reactRouterV7.js", + "types": "../cjs/entries/reactRouterV7.d.ts" +} diff --git a/packages/rum-react/src/domain/reactRooterTests/createRouter.spec.ts b/packages/rum-react/src/domain/reactRooterTests/createRouter.spec.ts new file mode 100644 index 0000000000..bc5cbce8b8 --- /dev/null +++ b/packages/rum-react/src/domain/reactRooterTests/createRouter.spec.ts @@ -0,0 +1,81 @@ +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import { createMemoryRouter as createMemoryRouterV6 } from '../../entries/reactRouterV6' +import { createMemoryRouter as createMemoryRouterV7 } from '../../entries/reactRouterV7' + +describe('createRouter', () => { + const versions = [ + { label: 'react-router v6', createMemoryRouter: createMemoryRouterV6 }, + { label: 'react-router v7', createMemoryRouter: createMemoryRouterV7 }, + ] + + for (const { label, createMemoryRouter } of versions) { + describe(label, () => { + let startViewSpy: jasmine.Spy<(name?: string | object) => void> + let router: ReturnType + + beforeEach(() => { + if (!window.AbortController) { + pending('createMemoryRouter relies on AbortController') + } + + startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + router: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + + router = createMemoryRouter( + [{ path: '/foo' }, { path: '/bar', children: [{ path: 'nested' }] }, { path: '*' }], + { + initialEntries: ['/foo'], + } + ) + }) + + afterEach(() => { + router?.dispose() + }) + + it('creates a new view when the router is created', () => { + expect(startViewSpy).toHaveBeenCalledWith('/foo') + }) + + it('creates a new view when the router navigates', async () => { + startViewSpy.calls.reset() + await router.navigate('/bar') + expect(startViewSpy).toHaveBeenCalledWith('/bar') + }) + + it('creates a new view when the router navigates to a nested route', async () => { + await router.navigate('/bar') + startViewSpy.calls.reset() + await router.navigate('/bar/nested') + expect(startViewSpy).toHaveBeenCalledWith('/bar/nested') + }) + + it('creates a new view with the fallback route', async () => { + startViewSpy.calls.reset() + await router.navigate('/non-existent') + expect(startViewSpy).toHaveBeenCalledWith('/non-existent') + }) + + it('does not create a new view when navigating to the same URL', async () => { + await router.navigate('/bar') + startViewSpy.calls.reset() + await router.navigate('/bar') + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('does not create a new view when just changing query parameters', async () => { + await router.navigate('/bar') + startViewSpy.calls.reset() + await router.navigate('/bar?baz=1') + expect(startViewSpy).not.toHaveBeenCalled() + }) + }) + } +}) diff --git a/packages/rum-react/src/domain/reactRooterTests/routesComponent.spec.tsx b/packages/rum-react/src/domain/reactRooterTests/routesComponent.spec.tsx new file mode 100644 index 0000000000..f7bfb2d557 --- /dev/null +++ b/packages/rum-react/src/domain/reactRooterTests/routesComponent.spec.tsx @@ -0,0 +1,205 @@ +import React from 'react' +import { flushSync } from 'react-dom' +import { MemoryRouter as MemoryRouterV6, Route as RouteV6, useNavigate as useNavigateV6 } from 'react-router-dom-6' +import { MemoryRouter as MemoryRouterV7, Route as RouteV7, useNavigate as useNavigateV7 } from 'react-router-dom-7' +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import { appendComponent } from '../../../test/appendComponent' +import { Routes as RoutesV6 } from '../../entries/reactRouterV6' +import { Routes as RoutesV7 } from '../../entries/reactRouterV7' +;[ + { + version: 'react-router-6', + MemoryRouter: MemoryRouterV6, + Route: RouteV6, + useNavigate: useNavigateV6, + Routes: RoutesV6, + }, + { + version: 'react-router-7', + MemoryRouter: MemoryRouterV7, + Route: RouteV7, + useNavigate: useNavigateV7, + Routes: RoutesV7, + }, +].forEach(({ version, MemoryRouter, Route, useNavigate, Routes }) => { + describe(`Routes component (${version})`, () => { + let startViewSpy: jasmine.Spy<(name?: string | object) => void> + + beforeEach(() => { + startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + router: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + }) + + it('starts a new view as soon as it is rendered', () => { + appendComponent( + + + + + + ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + }) + + it('renders the matching route', () => { + const container = appendComponent( + + + + + + ) + + expect(container.innerHTML).toBe('foo') + }) + + it('does not start a new view on re-render', () => { + let forceUpdate: () => void + + function App() { + const [, setState] = React.useState(0) + forceUpdate = () => setState((s) => s + 1) + return ( + + + + + + ) + } + + appendComponent() + + expect(startViewSpy).toHaveBeenCalledTimes(1) + + flushSync(() => { + forceUpdate!() + }) + + expect(startViewSpy).toHaveBeenCalledTimes(1) + }) + + it('starts a new view on navigation', async () => { + let navigate: (path: string) => void + + function NavBar() { + navigate = useNavigate() + return null + } + + appendComponent( + + + + + + + + ) + + startViewSpy.calls.reset() + flushSync(() => { + navigate!('/bar') + }) + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(startViewSpy).toHaveBeenCalledOnceWith('/bar') + }) + + it('does not start a new view if the URL is the same', () => { + let navigate: (path: string) => void + + function NavBar() { + navigate = useNavigate() + return null + } + + appendComponent( + + + + + + + ) + + startViewSpy.calls.reset() + flushSync(() => { + navigate!('/foo') + }) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('does not start a new view if the path is the same but with different parameters', () => { + let navigate: (path: string) => void + + function NavBar() { + navigate = useNavigate() + return null + } + + appendComponent( + + + + + + + ) + + startViewSpy.calls.reset() + flushSync(() => { + navigate!('/foo?bar=baz') + }) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('does not start a new view if it does not match any route', () => { + // Prevent react router from showing a warning in the console when a route does not match + spyOn(console, 'warn') + + appendComponent( + + + + + + ) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('allows passing a location object', () => { + appendComponent( + + + + + + ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + }) + + it('allows passing a location string', () => { + appendComponent( + + + + + + ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + }) + }) +}) diff --git a/packages/rum-react/src/domain/reactRooterTests/startReactRouterView.spec.ts b/packages/rum-react/src/domain/reactRooterTests/startReactRouterView.spec.ts new file mode 100644 index 0000000000..a13fe659ef --- /dev/null +++ b/packages/rum-react/src/domain/reactRooterTests/startReactRouterView.spec.ts @@ -0,0 +1,144 @@ +import { display } from '@datadog/browser-core' +import { + createMemoryRouter as createMemoryRouterV6, + type RouteObject as RouteObjectV6, + type RouteMatch as RouteMatchV6, +} from 'react-router-dom-6' +import { + createMemoryRouter as createMemoryRouterV7, + type RouteObject as RouteObjectV7, + type RouteMatch as RouteMatchV7, +} from 'react-router-dom-7' +import { registerCleanupTask } from '../../../../core/test' +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import * as ViewV6 from '../../entries/reactRouterV6' +import * as ViewV7 from '../../entries/reactRouterV7' + +const routerVersions = [ + { + version: 'react-router-6', + createMemoryRouter: createMemoryRouterV6, + startReactRouterView: ViewV6.startReactRouterView, + computeViewName: ViewV6.computeViewName, + }, + { + version: 'react-router-7', + createMemoryRouter: createMemoryRouterV7, + startReactRouterView: ViewV7.startReactRouterView, + computeViewName: ViewV7.computeViewName, + }, +] + +routerVersions.forEach(({ version, createMemoryRouter, startReactRouterView, computeViewName }) => { + describe(`startReactRouterView (${version})`, () => { + describe('startReactRouterView', () => { + it('creates a new view with the computed view name', () => { + const startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + router: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + + startReactRouterView([ + { route: { path: '/' } }, + { route: { path: 'user' } }, + { route: { path: ':id' } }, + ] as unknown as RouteMatchV6[] & RouteMatchV7[]) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/user/:id') + }) + + it('displays a warning if the router integration is not enabled', () => { + const displayWarnSpy = spyOn(display, 'warn') + initializeReactPlugin({ + configuration: {}, + }) + + startReactRouterView([] as unknown as RouteMatchV6[] & RouteMatchV7[]) + expect(displayWarnSpy).toHaveBeenCalledOnceWith( + '`router: true` is missing from the react plugin configuration, the view will not be tracked.' + ) + }) + }) + + describe('computeViewName', () => { + it('returns an empty string if there is no route match', () => { + expect(computeViewName([] as unknown as RouteMatchV6[] & RouteMatchV7[])).toBe('') + }) + + it('ignores routes without a path', () => { + expect( + computeViewName([ + { route: { path: '/foo' } }, + { route: {} }, + { route: { path: ':id' } }, + ] as unknown as RouteMatchV6[] & RouteMatchV7[]) + ).toBe('/foo/:id') + }) + + // prettier-ignore + const cases = [ + // route paths, path, expected view name + + // Simple paths + ['/foo', '/foo', '/foo'], + ['/foo', '/bar', '/foo'], // apparently when the path doesn't match any route, React Router returns the last route as a matching route + ['/foo > bar', '/foo/bar', '/foo/bar'], + ['/foo > bar > :p', '/foo/bar/1', '/foo/bar/:p'], + [':p', '/foo', '/:p'], + ['/foo/:p', '/foo/bar', '/foo/:p'], + ['/foo > :p', '/foo/bar', '/foo/:p'], + ['/:a/:b', '/foo/bar', '/:a/:b'], + ['/:a > :b', '/foo/bar', '/:a/:b'], + ['/foo-:a', '/foo-1', '/foo-:a'], + ['/foo/ > bar/ > :id/', '/foo/bar/1/', '/foo/bar/:id/'], + ['/foo > /foo/bar > /foo/bar/:id', + '/foo/bar/1', '/foo/bar/:id'], + + // Splats + ['*', '/foo/1', '/foo/1'], + ['*', '/', '/'], + ['/foo/*', '/foo/1', '/foo/1'], + ['/foo > *', '/foo/1', '/foo/1'], + ['* > *', '/foo/1', '/foo/1'], + ['* > *', '/', '/'], + ['/foo/* > *', '/foo/1', '/foo/1'], + ['* > foo/*', '/foo/1', '/foo/1'], + ['/foo/* > bar/*', '/foo/bar/1', '/foo/bar/1'], + ['/foo/* > bar', '/foo/bar', '/foo/bar'], + ['/foo/:p > *', '/foo/bar/baz', '/foo/:p/baz'], + ['/:p > *', '/foo/bar/1', '/:p/bar/1'], + ['/foo/* > :p', '/foo/bar', '/foo/:p'], + + // Extra edge cases - React Router does not provide the matched path in those case + ['/foo/*/bar', '/foo/1/bar', '/foo/*/bar'], + ['/foo/*-bar', '/foo/1-bar', '/foo/*-bar'], + ['*/*', '/foo/1', '/*/*'], + ] as const + + cases.forEach(([routePaths, path, expectedViewName]) => { + it(`compute the right view name for paths ${routePaths}`, () => { + // Convert the routePaths representing nested routes paths delimited by ' > ' to an actual + // react-router route object. Example: '/foo > bar > :p' would be turned into + // { path: '/foo', children: [{ path: 'bar', children: [{ path: ':p' }] }] } + const route = routePaths + .split(' > ') + .reduceRight( + (childRoute, routePath) => ({ path: routePath, children: childRoute ? [childRoute] : undefined }), + undefined as RouteObjectV6 | RouteObjectV7 | undefined + )! + + const router = createMemoryRouter([route] as any, { + initialEntries: [path], + }) + registerCleanupTask(() => router.dispose()) + expect(computeViewName(router.state.matches as any)).toEqual(expectedViewName) + }) + }) + }) + }) +}) diff --git a/packages/rum-react/src/domain/reactRooterTests/useRoutes.spec.tsx b/packages/rum-react/src/domain/reactRooterTests/useRoutes.spec.tsx new file mode 100644 index 0000000000..be9737d5ec --- /dev/null +++ b/packages/rum-react/src/domain/reactRooterTests/useRoutes.spec.tsx @@ -0,0 +1,272 @@ +import React from 'react' +import { flushSync } from 'react-dom' +import { MemoryRouter as MemoryRouterV6, useNavigate as useNavigateV6 } from 'react-router-dom-6' +import type { RouteObject as RouteObjectV6 } from 'react-router-dom-6' +import { MemoryRouter as MemoryRouterV7, useNavigate as useNavigateV7 } from 'react-router-dom-7' +import type { RouteObject as RouteObjectV7 } from 'react-router-dom-7' +import { appendComponent } from '../../../test/appendComponent' +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import { useRoutes as useRoutesV6 } from '../../entries/reactRouterV6' +import { useRoutes as useRoutesV7 } from '../../entries/reactRouterV7' + +const versions = [ + { + version: 'react-router-6', + MemoryRouter: MemoryRouterV6, + useNavigate: useNavigateV6, + useRoutes: useRoutesV6, + }, + { + version: 'react-router-7', + MemoryRouter: MemoryRouterV7, + useNavigate: useNavigateV7, + useRoutes: useRoutesV7, + }, +] + +versions.forEach(({ version, MemoryRouter, useNavigate, useRoutes }) => { + describe(`useRoutes (${version})`, () => { + let startViewSpy: jasmine.Spy<(name?: string | object) => void> + + beforeEach(() => { + startViewSpy = jasmine.createSpy() + initializeReactPlugin({ + configuration: { + router: true, + }, + publicApi: { + startView: startViewSpy, + }, + }) + }) + + it('starts a new view as soon as it is rendered', () => { + appendComponent( + + + + ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + }) + + it('renders the matching route', () => { + const container = appendComponent( + + + + ) + + expect(container.innerHTML).toBe('foo') + }) + + it('does not start a new view on re-render', () => { + let forceUpdate: () => void + + function App() { + const [, setState] = React.useState(0) + forceUpdate = () => setState((s) => s + 1) + return ( + + + + ) + } + + appendComponent() + + expect(startViewSpy).toHaveBeenCalledTimes(1) + + flushSync(() => { + forceUpdate!() + }) + + expect(startViewSpy).toHaveBeenCalledTimes(1) + }) + + it('starts a new view on navigation', async () => { + let navigate: (path: string) => void + + function NavBar() { + navigate = useNavigate() + return null + } + + appendComponent( + + + + + ) + + startViewSpy.calls.reset() + flushSync(() => { + navigate!('/bar') + }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(startViewSpy).toHaveBeenCalledOnceWith('/bar') + }) + + it('does not start a new view if the URL is the same', () => { + let navigate: (path: string) => void + + function NavBar() { + navigate = useNavigate() + return null + } + + appendComponent( + + + + + ) + + startViewSpy.calls.reset() + flushSync(() => { + navigate!('/foo') + }) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('does not start a new view if the path is the same but with different parameters', () => { + let navigate: (path: string) => void + + function NavBar() { + navigate = useNavigate() + return null + } + + appendComponent( + + + + + ) + + startViewSpy.calls.reset() + flushSync(() => { + navigate!('/foo?bar=baz') + }) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('does not start a new view if it does not match any route', () => { + // Prevent react router from showing a warning in the console when a route does not match + spyOn(console, 'warn') + + appendComponent( + + + + ) + + expect(startViewSpy).not.toHaveBeenCalled() + }) + + it('allows passing a location object', () => { + appendComponent( + + + + ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + }) + + it('allows passing a location string', () => { + appendComponent( + + + + ) + + expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') + }) + }) +}) + +function RoutesRenderer({ + routes, + location, + useRoutes, +}: { + routes: RouteObjectV6[] & RouteObjectV7[] + location?: { pathname: string } | string + useRoutes: typeof useRoutesV6 | typeof useRoutesV7 +}) { + return useRoutes(routes, location) +} diff --git a/packages/rum-react/src/domain/reactRouter/createRouter.ts b/packages/rum-react/src/domain/reactRouter/createRouter.ts new file mode 100644 index 0000000000..e0786c40e3 --- /dev/null +++ b/packages/rum-react/src/domain/reactRouter/createRouter.ts @@ -0,0 +1,20 @@ +import { startReactRouterView } from './startReactRouterView' +import type { AnyCreateRouter } from './types' + +export function wrapCreateRouter>( + originalCreateRouter: CreateRouter +): CreateRouter { + return ((routes, options) => { + const router = originalCreateRouter(routes, options) + let location = router.state.location.pathname + router.subscribe((routerState) => { + const newPathname = routerState.location.pathname + if (location !== newPathname) { + startReactRouterView(routerState.matches) + location = newPathname + } + }) + startReactRouterView(router.state.matches) + return router + }) as CreateRouter +} diff --git a/packages/rum-react/src/domain/reactRouter/index.ts b/packages/rum-react/src/domain/reactRouter/index.ts new file mode 100644 index 0000000000..6984407e86 --- /dev/null +++ b/packages/rum-react/src/domain/reactRouter/index.ts @@ -0,0 +1,4 @@ +export { wrapCreateRouter } from './createRouter' +export { wrapUseRoutes } from './useRoutes' +export { createRoutesComponent } from './routesComponent' +export { startReactRouterView, computeViewName } from './startReactRouterView' diff --git a/packages/rum-react/src/domain/reactRouter/routesComponent.ts b/packages/rum-react/src/domain/reactRouter/routesComponent.ts new file mode 100644 index 0000000000..6916e4920c --- /dev/null +++ b/packages/rum-react/src/domain/reactRouter/routesComponent.ts @@ -0,0 +1,11 @@ +import type { AnyLocation, AnyRouteObject, AnyUseRoute } from './types' +// Same as react-router-dom Routes but with our useRoutes instead of the original one +// https://github.com/remix-run/react-router/blob/5d66dbdbc8edf1d9c3a4d9c9d84073d046b5785b/packages/react-router/lib/components.tsx#L503-L508 +export function createRoutesComponent( + useRoutes: AnyUseRoute, + createRoutesFromChildren: (children: React.ReactNode, parentPath?: number[]) => AnyRouteObject[] +) { + return function Routes({ children, location }: { children: React.ReactNode; location?: Location }) { + return useRoutes(createRoutesFromChildren(children), location) + } +} diff --git a/packages/rum-react/src/domain/reactRouterV6/startReactRouterView.ts b/packages/rum-react/src/domain/reactRouter/startReactRouterView.ts similarity index 69% rename from packages/rum-react/src/domain/reactRouterV6/startReactRouterView.ts rename to packages/rum-react/src/domain/reactRouter/startReactRouterView.ts index 7961762e50..6b2b1157a1 100644 --- a/packages/rum-react/src/domain/reactRouterV6/startReactRouterView.ts +++ b/packages/rum-react/src/domain/reactRouter/startReactRouterView.ts @@ -1,8 +1,8 @@ -import type { RouteMatch } from 'react-router-dom' import { display } from '@datadog/browser-core' import { onRumInit } from '../reactPlugin' +import type { AnyRouteMatch } from './types' -export function startReactRouterView(routeMatches: RouteMatch[]) { +export function startReactRouterView(routeMatches: AnyRouteMatch[]) { onRumInit((configuration, rumPublicApi) => { if (!configuration.router) { display.warn('`router: true` is missing from the react plugin configuration, the view will not be tracked.') @@ -12,7 +12,7 @@ export function startReactRouterView(routeMatches: RouteMatch[]) { }) } -export function computeViewName(routeMatches: RouteMatch[]) { +export function computeViewName(routeMatches: AnyRouteMatch[]) { if (!routeMatches || routeMatches.length === 0) { return '' } @@ -54,22 +54,13 @@ export function computeViewName(routeMatches: RouteMatch[]) { * substitutePathSplats('/files/*', { '*': 'path/to/file' }, true) // => '/files/path/to/file' */ function substitutePathSplats(path: string, params: Record, isLastMatchingRoute: boolean) { - if ( - !path.includes('*') || - // In some edge cases, react-router does not provide the `*` parameter, so we don't know what to - // replace it with. In this case, we keep the asterisk. - params['*'] === undefined - ) { + if (!path.includes('*') || params['*'] === undefined) { return path } - // The `*` parameter is only related to the last matching route path. if (isLastMatchingRoute) { return path.replace(/\*/, params['*']) } - // Intermediary route paths with a `*` are kind of edge cases, and the `*` parameter is not - // relevant for them. We remove it from the path (along with a potential slash preceeding it) to - // have a coherent view name once everything is concatenated (see examples in spec file). return path.replace(/\/?\*/, '') } diff --git a/packages/rum-react/src/domain/reactRouter/types.ts b/packages/rum-react/src/domain/reactRouter/types.ts new file mode 100644 index 0000000000..a05538a4e1 --- /dev/null +++ b/packages/rum-react/src/domain/reactRouter/types.ts @@ -0,0 +1,14 @@ +export type AnyRouteObject = { path?: string | undefined } +export type AnyUseRoute = ( + routes: AnyRouteObject[], + location?: Location +) => React.ReactElement | null +export type AnyRouteMatch = { route: AnyRouteObject; params: Record } +export type AnyLocation = { pathname: string } | string +export type AnyCreateRouter = ( + routes: AnyRouteObject[], + options?: Options +) => { + state: { location: { pathname: string }; matches: AnyRouteMatch[] } + subscribe: (callback: (routerState: { location: { pathname: string }; matches: AnyRouteMatch[] }) => void) => void +} diff --git a/packages/rum-react/src/domain/reactRouter/useRoutes.ts b/packages/rum-react/src/domain/reactRouter/useRoutes.ts new file mode 100644 index 0000000000..baadc88b83 --- /dev/null +++ b/packages/rum-react/src/domain/reactRouter/useRoutes.ts @@ -0,0 +1,29 @@ +import { useRef } from 'react' +import { startReactRouterView } from './startReactRouterView' +import type { AnyUseRoute, AnyRouteObject, AnyRouteMatch } from './types' + +export function wrapUseRoutes>({ + useRoutes, + useLocation, + matchRoutes, +}: { + useRoutes: T + useLocation: () => { pathname: string } + matchRoutes: (routes: AnyRouteObject[], pathname: string) => AnyRouteMatch[] | null +}): T { + return ((routes, locationArg) => { + const location = useLocation() + const pathname = typeof locationArg === 'string' ? locationArg : locationArg?.pathname || location.pathname + const pathnameRef = useRef(null) + + if (pathnameRef.current !== pathname) { + pathnameRef.current = pathname + const matchedRoutes = matchRoutes(routes, pathname) + if (matchedRoutes) { + startReactRouterView(matchedRoutes) + } + } + + return useRoutes(routes, locationArg) + }) as T +} diff --git a/packages/rum-react/src/domain/reactRouterV6/createRouter.spec.ts b/packages/rum-react/src/domain/reactRouterV6/createRouter.spec.ts deleted file mode 100644 index 2a673ae330..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/createRouter.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { initializeReactPlugin } from '../../../test/initializeReactPlugin' -import { createMemoryRouter } from './createRouter' - -describe('createRouter', () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> - let router: ReturnType - - beforeEach(() => { - if (!window.AbortController) { - pending('createMemoryRouter rely on AbortController') - } - - startViewSpy = jasmine.createSpy() - initializeReactPlugin({ - configuration: { - router: true, - }, - publicApi: { - startView: startViewSpy, - }, - }) - - router = createMemoryRouter([{ path: '/foo' }, { path: '/bar', children: [{ path: 'nested' }] }, { path: '*' }], { - initialEntries: ['/foo'], - }) - }) - - afterEach(() => { - router?.dispose() - }) - - it('creates a new view when the router is created', () => { - expect(startViewSpy).toHaveBeenCalledWith('/foo') - }) - - it('creates a new view when the router navigates', async () => { - startViewSpy.calls.reset() - await router.navigate('/bar') - expect(startViewSpy).toHaveBeenCalledWith('/bar') - }) - - it('creates a new view when the router navigates to a nested route', async () => { - await router.navigate('/bar') - startViewSpy.calls.reset() - await router.navigate('/bar/nested') - expect(startViewSpy).toHaveBeenCalledWith('/bar/nested') - }) - - it('creates a new view with the fallback route', async () => { - startViewSpy.calls.reset() - await router.navigate('/non-existent') - expect(startViewSpy).toHaveBeenCalledWith('/non-existent') - }) - - it('does not create a new view when the router navigates to the same URL', async () => { - await router.navigate('/bar') - startViewSpy.calls.reset() - await router.navigate('/bar') - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('does not create a new view when the router navigates to the same path but different parameters', async () => { - await router.navigate('/bar') - startViewSpy.calls.reset() - await router.navigate('/bar?baz=1') - expect(startViewSpy).not.toHaveBeenCalled() - }) -}) diff --git a/packages/rum-react/src/domain/reactRouterV6/createRouter.ts b/packages/rum-react/src/domain/reactRouterV6/createRouter.ts deleted file mode 100644 index e7bf5c7fc2..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/createRouter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - createBrowserRouter as originalCreateBrowserRouter, - createHashRouter as originalCreateHashRouter, - createMemoryRouter as originalCreateMemoryRouter, -} from 'react-router-dom' -import { startReactRouterView } from './startReactRouterView' - -type Router = ReturnType - -export const createBrowserRouter: typeof originalCreateBrowserRouter = (routes, options) => - registerRouter(originalCreateBrowserRouter(routes, options)) -export const createHashRouter: typeof originalCreateHashRouter = (routes, options) => - registerRouter(originalCreateHashRouter(routes, options)) -export const createMemoryRouter: typeof originalCreateMemoryRouter = (routes, options) => - registerRouter(originalCreateMemoryRouter(routes, options)) - -export function registerRouter(router: Router) { - let location = router.state.location.pathname - router.subscribe((routerState) => { - const newPathname = routerState.location.pathname - if (location !== newPathname) { - startReactRouterView(routerState.matches) - location = newPathname - } - }) - startReactRouterView(router.state.matches) - return router -} diff --git a/packages/rum-react/src/domain/reactRouterV6/index.ts b/packages/rum-react/src/domain/reactRouterV6/index.ts deleted file mode 100644 index 68f2aa12ba..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { createMemoryRouter, createHashRouter, createBrowserRouter } from './createRouter' -export { useRoutes } from './useRoutes' -export { Routes } from './routesComponent' diff --git a/packages/rum-react/src/domain/reactRouterV6/routesComponent.spec.tsx b/packages/rum-react/src/domain/reactRouterV6/routesComponent.spec.tsx deleted file mode 100644 index f169f7be3e..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/routesComponent.spec.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React from 'react' -import { flushSync } from 'react-dom' - -import { MemoryRouter, Route, useNavigate } from 'react-router-dom' -import { initializeReactPlugin } from '../../../test/initializeReactPlugin' -import { appendComponent } from '../../../test/appendComponent' -import { Routes } from './routesComponent' - -describe('Routes component', () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> - - beforeEach(() => { - startViewSpy = jasmine.createSpy() - initializeReactPlugin({ - configuration: { - router: true, - }, - publicApi: { - startView: startViewSpy, - }, - }) - }) - - it('starts a new view as soon as it is rendered', () => { - appendComponent( - - - - - - ) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') - }) - - it('renders the matching route', () => { - const container = appendComponent( - - - - - - ) - - expect(container.innerHTML).toBe('foo') - }) - - it('does not start a new view on re-render', () => { - let forceUpdate: () => void - - function App() { - const [, setState] = React.useState(0) - forceUpdate = () => setState((s) => s + 1) - return ( - - - - - - ) - } - - appendComponent() - - expect(startViewSpy).toHaveBeenCalledTimes(1) - - flushSync(() => { - forceUpdate!() - }) - - expect(startViewSpy).toHaveBeenCalledTimes(1) - }) - - it('starts a new view on navigation', () => { - let navigate: (path: string) => void - - function NavBar() { - navigate = useNavigate() - - return null - } - - appendComponent( - - - - - - - - ) - - startViewSpy.calls.reset() - flushSync(() => { - navigate!('/bar') - }) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/bar') - }) - - it('does not start a new view if the URL is the same', () => { - let navigate: (path: string) => void - - function NavBar() { - navigate = useNavigate() - - return null - } - - appendComponent( - - - - - - - ) - - startViewSpy.calls.reset() - flushSync(() => { - navigate!('/foo') - }) - - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('does not start a new view if the path is the same but with different parameters', () => { - let navigate: (path: string) => void - - function NavBar() { - navigate = useNavigate() - - return null - } - - appendComponent( - - - - - - - ) - - startViewSpy.calls.reset() - flushSync(() => { - navigate!('/foo?bar=baz') - }) - - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('does not start a new view if it does not match any route', () => { - // Prevent react router from showing a warning in the console when a route does not match - spyOn(console, 'warn') - - appendComponent( - - - - - - ) - - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('allows passing a location object', () => { - appendComponent( - - - - - - ) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') - }) - - it('allows passing a location string', () => { - appendComponent( - - - - - - ) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') - }) -}) diff --git a/packages/rum-react/src/domain/reactRouterV6/routesComponent.ts b/packages/rum-react/src/domain/reactRouterV6/routesComponent.ts deleted file mode 100644 index 354f3db95f..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/routesComponent.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Routes as OriginalRoutes } from 'react-router-dom' -import { createRoutesFromChildren } from 'react-router-dom' -import { useRoutes } from './useRoutes' - -// Same as react-router-dom Routes but with our useRoutes instead of the original one -// https://github.com/remix-run/react-router/blob/5d66dbdbc8edf1d9c3a4d9c9d84073d046b5785b/packages/react-router/lib/components.tsx#L503-L508 -export const Routes: typeof OriginalRoutes = ({ children, location }) => - useRoutes(createRoutesFromChildren(children), location) diff --git a/packages/rum-react/src/domain/reactRouterV6/startReactRouterView.spec.ts b/packages/rum-react/src/domain/reactRouterV6/startReactRouterView.spec.ts deleted file mode 100644 index 315bf81a3b..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/startReactRouterView.spec.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { type RouteObject, createMemoryRouter, type RouteMatch } from 'react-router-dom' -import { display } from '@datadog/browser-core' -import { registerCleanupTask } from '../../../../core/test' -import { initializeReactPlugin } from '../../../test/initializeReactPlugin' -import { computeViewName, startReactRouterView } from './startReactRouterView' - -describe('startReactRouterView', () => { - it('creates a new view with the computed view name', () => { - const startViewSpy = jasmine.createSpy() - initializeReactPlugin({ - configuration: { - router: true, - }, - publicApi: { - startView: startViewSpy, - }, - }) - - startReactRouterView([ - { route: { path: '/' } }, - { route: { path: 'user' } }, - { route: { path: ':id' } }, - ] as RouteMatch[]) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/user/:id') - }) - - it('displays a warning if the router integration is not enabled', () => { - const displayWarnSpy = spyOn(display, 'warn') - initializeReactPlugin({ - configuration: {}, - }) - - startReactRouterView([] as RouteMatch[]) - expect(displayWarnSpy).toHaveBeenCalledOnceWith( - '`router: true` is missing from the react plugin configuration, the view will not be tracked.' - ) - }) -}) - -describe('computeViewName', () => { - it('returns an empty string if there is no route match', () => { - expect(computeViewName([] as RouteMatch[])).toBe('') - }) - - it('ignores routes without a path', () => { - expect( - computeViewName([{ route: { path: '/foo' } }, { route: {} }, { route: { path: ':id' } }] as RouteMatch[]) - ).toBe('/foo/:id') - }) - - // prettier-ignore - const cases = [ - // route paths, path, expected view name - - // Simple paths - ['/foo', '/foo', '/foo'], - ['/foo', '/bar', '/foo'], // apparently when the path doesn't match any route, React Router returns the last route as a matching route - ['/foo > bar', '/foo/bar', '/foo/bar'], - ['/foo > bar > :p', '/foo/bar/1', '/foo/bar/:p'], - [':p', '/foo', '/:p'], - ['/foo/:p', '/foo/bar', '/foo/:p'], - ['/foo > :p', '/foo/bar', '/foo/:p'], - ['/:a/:b', '/foo/bar', '/:a/:b'], - ['/:a > :b', '/foo/bar', '/:a/:b'], - ['/foo-:a', '/foo-1', '/foo-:a'], - ['/foo/ > bar/ > :id/', '/foo/bar/1/', '/foo/bar/:id/'], - ['/foo > /foo/bar > /foo/bar/:id', - '/foo/bar/1', '/foo/bar/:id'], - - // Splats - ['*', '/foo/1', '/foo/1'], - ['*', '/', '/'], - ['/foo/*', '/foo/1', '/foo/1'], - ['/foo > *', '/foo/1', '/foo/1'], - ['* > *', '/foo/1', '/foo/1'], - ['* > *', '/', '/'], - ['/foo/* > *', '/foo/1', '/foo/1'], - ['* > foo/*', '/foo/1', '/foo/1'], - ['/foo/* > bar/*', '/foo/bar/1', '/foo/bar/1'], - ['/foo/* > bar', '/foo/bar', '/foo/bar'], - ['/foo/:p > *', '/foo/bar/baz', '/foo/:p/baz'], - ['/:p > *', '/foo/bar/1', '/:p/bar/1'], - ['/foo/* > :p', '/foo/bar', '/foo/:p'], - - // Extra edge cases - React Router does not provide the matched path in those case - ['/foo/*/bar', '/foo/1/bar', '/foo/*/bar'], - ['/foo/*-bar', '/foo/1-bar', '/foo/*-bar'], - ['*/*', '/foo/1', '/*/*'], - ] as const - - cases.forEach(([routePaths, path, expectedViewName]) => { - it(`compute the right view name for paths ${routePaths}`, () => { - // Convert the routePaths representing nested routes paths delimited by ' > ' to an actual - // react-router route object. Example: '/foo > bar > :p' would be turned into - // { path: '/foo', children: [{ path: 'bar', children: [{ path: ':p' }] }] } - const route = routePaths - .split(' > ') - .reduceRight( - (childRoute, path) => ({ path, children: childRoute ? [childRoute] : undefined }), - undefined as RouteObject | undefined - )! - - const router = createMemoryRouter([route], { - initialEntries: [path], - }) - registerCleanupTask(() => router.dispose()) - expect(computeViewName(router.state.matches)).toEqual(expectedViewName) - }) - }) -}) diff --git a/packages/rum-react/src/domain/reactRouterV6/useRoutes.spec.tsx b/packages/rum-react/src/domain/reactRouterV6/useRoutes.spec.tsx deleted file mode 100644 index 27998ddef3..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/useRoutes.spec.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React from 'react' -import { flushSync } from 'react-dom' - -import type { RouteObject } from 'react-router-dom' -import { MemoryRouter, useNavigate } from 'react-router-dom' -import { initializeReactPlugin } from '../../../test/initializeReactPlugin' -import { appendComponent } from '../../../test/appendComponent' -import { useRoutes } from './useRoutes' - -describe('useRoutes', () => { - let startViewSpy: jasmine.Spy<(name?: string | object) => void> - - beforeEach(() => { - startViewSpy = jasmine.createSpy() - initializeReactPlugin({ - configuration: { - router: true, - }, - publicApi: { - startView: startViewSpy, - }, - }) - }) - - it('starts a new view as soon as it is rendered', () => { - appendComponent( - - - - ) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') - }) - - it('renders the matching route', () => { - const container = appendComponent( - - - - ) - - expect(container.innerHTML).toBe('foo') - }) - - it('does not start a new view on re-render', () => { - let forceUpdate: () => void - - function App() { - const [, setState] = React.useState(0) - forceUpdate = () => setState((s) => s + 1) - return ( - - - - ) - } - - appendComponent() - - expect(startViewSpy).toHaveBeenCalledTimes(1) - - flushSync(() => { - forceUpdate!() - }) - - expect(startViewSpy).toHaveBeenCalledTimes(1) - }) - - it('starts a new view on navigation', () => { - let navigate: (path: string) => void - - function NavBar() { - navigate = useNavigate() - - return null - } - - appendComponent( - - - - - ) - - startViewSpy.calls.reset() - flushSync(() => { - navigate!('/bar') - }) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/bar') - }) - - it('does not start a new view if the URL is the same', () => { - let navigate: (path: string) => void - - function NavBar() { - navigate = useNavigate() - - return null - } - - appendComponent( - - - - - ) - - startViewSpy.calls.reset() - flushSync(() => { - navigate!('/foo') - }) - - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('does not start a new view if the path is the same but with different parameters', () => { - let navigate: (path: string) => void - - function NavBar() { - navigate = useNavigate() - - return null - } - - appendComponent( - - - - - ) - - startViewSpy.calls.reset() - flushSync(() => { - navigate!('/foo?bar=baz') - }) - - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('does not start a new view if it does not match any route', () => { - // Prevent react router from showing a warning in the console when a route does not match - spyOn(console, 'warn') - - appendComponent( - - - - ) - - expect(startViewSpy).not.toHaveBeenCalled() - }) - - it('allows passing a location object', () => { - appendComponent( - - - - ) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') - }) - - it('allows passing a location string', () => { - appendComponent( - - - - ) - - expect(startViewSpy).toHaveBeenCalledOnceWith('/foo') - }) -}) - -function RoutesRenderer({ routes, location }: { routes: RouteObject[]; location?: { pathname: string } | string }) { - return useRoutes(routes, location) -} diff --git a/packages/rum-react/src/domain/reactRouterV6/useRoutes.ts b/packages/rum-react/src/domain/reactRouterV6/useRoutes.ts deleted file mode 100644 index 588405dda9..0000000000 --- a/packages/rum-react/src/domain/reactRouterV6/useRoutes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useRef } from 'react' -import { matchRoutes, useLocation, useRoutes as originalUseRoutes } from 'react-router-dom' -import { startReactRouterView } from './startReactRouterView' - -export const useRoutes: typeof originalUseRoutes = (routes, locationArg) => { - const location = useLocation() - const pathname = typeof locationArg === 'string' ? locationArg : locationArg?.pathname || location.pathname - const pathnameRef = useRef(null) - - if (pathnameRef.current !== pathname) { - pathnameRef.current = pathname - const matchedRoutes = matchRoutes(routes, pathname) - if (matchedRoutes) { - startReactRouterView(matchedRoutes) - } - } - - return originalUseRoutes(routes, locationArg) -} diff --git a/packages/rum-react/src/entries/reactRouterV6.ts b/packages/rum-react/src/entries/reactRouterV6.ts index 37e4e7ab39..2f28b39c1b 100644 --- a/packages/rum-react/src/entries/reactRouterV6.ts +++ b/packages/rum-react/src/entries/reactRouterV6.ts @@ -1 +1,26 @@ -export * from '../domain/reactRouterV6' +/* eslint-disable local-rules/disallow-side-effects */ + +import { + createBrowserRouter as originalCreateBrowserRouter, + createHashRouter as originalCreateHashRouter, + createMemoryRouter as originalCreateMemoryRouter, + useRoutes as originalUseRoutes, + useLocation, + matchRoutes, + createRoutesFromChildren, +} from 'react-router-dom-6' +import { wrapCreateRouter, createRoutesComponent, wrapUseRoutes } from '../domain/reactRouter' + +export const createBrowserRouter = wrapCreateRouter(originalCreateBrowserRouter) +export const createHashRouter = wrapCreateRouter(originalCreateHashRouter) +export const createMemoryRouter = wrapCreateRouter(originalCreateMemoryRouter) + +export const useRoutes = wrapUseRoutes({ + useRoutes: originalUseRoutes, + useLocation, + matchRoutes, +}) + +export const Routes = createRoutesComponent(useRoutes, createRoutesFromChildren) + +export * from '../domain/reactRouter' diff --git a/packages/rum-react/src/entries/reactRouterV7.ts b/packages/rum-react/src/entries/reactRouterV7.ts new file mode 100644 index 0000000000..a087154157 --- /dev/null +++ b/packages/rum-react/src/entries/reactRouterV7.ts @@ -0,0 +1,26 @@ +/* eslint-disable local-rules/disallow-side-effects */ + +import { + createBrowserRouter as originalCreateBrowserRouter, + createHashRouter as originalCreateHashRouter, + createMemoryRouter as originalCreateMemoryRouter, + useRoutes as originalUseRoutes, + useLocation, + matchRoutes, + createRoutesFromChildren, +} from 'react-router-dom-7' +import { wrapCreateRouter, createRoutesComponent, wrapUseRoutes } from '../domain/reactRouter' + +export const createBrowserRouter = wrapCreateRouter(originalCreateBrowserRouter) +export const createHashRouter = wrapCreateRouter(originalCreateHashRouter) +export const createMemoryRouter = wrapCreateRouter(originalCreateMemoryRouter) + +export const useRoutes = wrapUseRoutes({ + useRoutes: originalUseRoutes, + useLocation, + matchRoutes, +}) + +export const Routes = createRoutesComponent(useRoutes, createRoutesFromChildren) + +export * from '../domain/reactRouter' diff --git a/sandbox/react-app/main.tsx b/sandbox/react-app/main.tsx index 07188e9b33..8c88f4d7f2 100644 --- a/sandbox/react-app/main.tsx +++ b/sandbox/react-app/main.tsx @@ -1,8 +1,8 @@ -import { Link, Outlet, RouterProvider, useParams } from 'react-router-dom' +import { Link, Outlet, RouterProvider, useParams } from 'react-router-dom-7' import React, { useEffect, useState } from 'react' import ReactDOM from 'react-dom/client' import { datadogRum } from '@datadog/browser-rum' -import { createBrowserRouter } from '@datadog/browser-rum-react/react-router-v6' +import { createBrowserRouter } from '@datadog/browser-rum-react/react-router-v7' import { reactPlugin, ErrorBoundary, UNSTABLE_ReactComponentTracker } from '@datadog/browser-rum-react' datadogRum.init({ diff --git a/tsconfig.base.json b/tsconfig.base.json index 8230854f3a..56005e1f71 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -11,6 +11,7 @@ "target": "ES2018", "sourceMap": true, "jsx": "react", + "skipLibCheck": true, "types": [], "lib": ["ES2020", "DOM"], @@ -19,7 +20,8 @@ "@datadog/browser-core": ["./packages/core/src"], "@datadog/browser-rum-core": ["./packages/rum-core/src"], "@datadog/browser-rum-react": ["./packages/rum-react/src/entries/main"], - "@datadog/browser-rum-react/react-router-v6": ["./packages/rum-react/src/entries/reactRouterV6"] + "@datadog/browser-rum-react/react-router-v6": ["./packages/rum-react/src/entries/reactRouterV6"], + "@datadog/browser-rum-react/react-router-v7": ["./packages/rum-react/src/entries/reactRouterV7"] } } } diff --git a/yarn.lock b/yarn.lock index 7c95732835..befa2f6430 100644 --- a/yarn.lock +++ b/yarn.lock @@ -355,10 +355,11 @@ __metadata: "@types/react-dom": "npm:18.3.5" react: "npm:18.3.1" react-dom: "npm:18.3.1" - react-router-dom: "npm:6.28.2" + react-router-dom-6: "npm:react-router-dom@6.28.2" + react-router-dom-7: "npm:react-router-dom@7.1.3" peerDependencies: react: 18 - react-router-dom: 6 + react-router-dom: 6 || 7 peerDependenciesMeta: "@datadog/browser-rum": optional: true @@ -1769,6 +1770,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/cookie@npm:0.6.0" + checksum: 10c0/5b326bd0188120fb32c0be086b141b1481fec9941b76ad537f9110e10d61ee2636beac145463319c71e4be67a17e85b81ca9e13ceb6e3bb63b93d16824d6c149 + languageName: node + linkType: hard + "@types/cors@npm:2.8.17, @types/cors@npm:^2.8.12": version: 2.8.17 resolution: "@types/cors@npm:2.8.17" @@ -4382,6 +4390,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^1.0.1": + version: 1.0.2 + resolution: "cookie@npm:1.0.2" + checksum: 10c0/fd25fe79e8fbcfcaf6aa61cd081c55d144eeeba755206c058682257cb38c4bd6795c6620de3f064c740695bb65b7949ebb1db7a95e4636efb8357a335ad3f54b + languageName: node + linkType: hard + "cookie@npm:~0.4.1": version: 0.4.2 resolution: "cookie@npm:0.4.2" @@ -11287,7 +11302,7 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:6.28.2": +"react-router-dom-6@npm:react-router-dom@6.28.2": version: 6.28.2 resolution: "react-router-dom@npm:6.28.2" dependencies: @@ -11300,6 +11315,18 @@ __metadata: languageName: node linkType: hard +"react-router-dom-7@npm:react-router-dom@7.1.3": + version: 7.1.3 + resolution: "react-router-dom@npm:7.1.3" + dependencies: + react-router: "npm:7.1.3" + peerDependencies: + react: ">=18" + react-dom: ">=18" + checksum: 10c0/84752b90e3f9e9168fc29d7dd5eb0ff622a831de15e4e5d899694f19bc988387d831d180d07f503071d944fa4a666c1d07004e4526268215cc5dbe5bba98f52c + languageName: node + linkType: hard + "react-router@npm:6.28.2": version: 6.28.2 resolution: "react-router@npm:6.28.2" @@ -11311,6 +11338,24 @@ __metadata: languageName: node linkType: hard +"react-router@npm:7.1.3": + version: 7.1.3 + resolution: "react-router@npm:7.1.3" + dependencies: + "@types/cookie": "npm:^0.6.0" + cookie: "npm:^1.0.1" + set-cookie-parser: "npm:^2.6.0" + turbo-stream: "npm:2.4.0" + peerDependencies: + react: ">=18" + react-dom: ">=18" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10c0/f42f7b245533d1adaa00779a0287993a836d5b56039d97a6643a8b3a721ffb92ff47c97cfb36409fec8794ac3c8a884339f588cf21fcd7f6dccdfc834520c76f + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1, react-style-singleton@npm:^2.2.2": version: 2.2.3 resolution: "react-style-singleton@npm:2.2.3" @@ -11995,6 +12040,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.6.0": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: 10c0/060c198c4c92547ac15988256f445eae523f57f2ceefeccf52d30d75dedf6bff22b9c26f756bd44e8e560d44ff4ab2130b178bd2e52ef5571bf7be3bd7632d9a + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -13076,6 +13128,13 @@ __metadata: languageName: node linkType: hard +"turbo-stream@npm:2.4.0": + version: 2.4.0 + resolution: "turbo-stream@npm:2.4.0" + checksum: 10c0/e68b2569f1f16e6e9633d090c6024b2ae9f0e97bfeacb572451ca3732e120ebbb546f3bc4afc717c46cb57b5aea6104e04ef497f9912eef6a7641e809518e98a + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0"