diff --git a/packages/rum-react/archive.tgz b/packages/rum-react/archive.tgz new file mode 100644 index 0000000000..012485c9a4 Binary files /dev/null and b/packages/rum-react/archive.tgz differ diff --git a/packages/rum-react/package.json b/packages/rum-react/package.json index 1ccb3b3cab..49024c3323 100644 --- a/packages/rum-react/package.json +++ b/packages/rum-react/package.json @@ -13,7 +13,8 @@ }, "dependencies": { "@datadog/browser-core": "5.34.1", - "@datadog/browser-rum-core": "5.34.1" + "@datadog/browser-rum-core": "5.34.1", + "uuid": "10.0.0" }, "peerDependencies": { "react": "18", @@ -36,6 +37,7 @@ "devDependencies": { "@types/react": "18.3.17", "@types/react-dom": "18.3.5", + "@types/uuid": "^10", "react": "18.3.1", "react-dom": "18.3.1", "react-router-dom": "6.28.0" diff --git a/packages/rum-react/src/domain/performance/addDurationVital.ts b/packages/rum-react/src/domain/performance/addDurationVital.ts new file mode 100644 index 0000000000..91ca505152 --- /dev/null +++ b/packages/rum-react/src/domain/performance/addDurationVital.ts @@ -0,0 +1,8 @@ +import type { RumPublicApi } from '@datadog/browser-rum-core' +import { onReactPluginInit } from '../reactPlugin' + +export const addDurationVital: RumPublicApi['addDurationVital'] = (name, options) => { + onReactPluginInit((_, rumPublicApi) => { + rumPublicApi.addDurationVital(name, options) + }) +} diff --git a/packages/rum-react/src/domain/performance/getTimer.spec.ts b/packages/rum-react/src/domain/performance/getTimer.spec.ts new file mode 100644 index 0000000000..8546bbf2b6 --- /dev/null +++ b/packages/rum-react/src/domain/performance/getTimer.spec.ts @@ -0,0 +1,13 @@ +import { getTimer } from './getTimer' + +describe('getTimer', () => { + it('is able to measure time', () => { + const timer = getTimer() + + timer.startTimer() + setTimeout(() => { + timer.stopTimer() + expect(timer.getDuration()).toBeGreaterThan(1000) + }, 1000) + }) +}) diff --git a/packages/rum-react/src/domain/performance/getTimer.ts b/packages/rum-react/src/domain/performance/getTimer.ts new file mode 100644 index 0000000000..896f5055fa --- /dev/null +++ b/packages/rum-react/src/domain/performance/getTimer.ts @@ -0,0 +1,23 @@ +export function getTimer() { + let duration: number + let startTime: number + + function startTimer() { + const start = performance.now() + startTime = performance.timeOrigin + start + } + + function stopTimer() { + duration = performance.timeOrigin + performance.now() - startTime + } + + function getDuration() { + return duration ? duration : 0 + } + + function getStartTime() { + return startTime + } + + return { startTimer, stopTimer, getDuration, getStartTime } +} diff --git a/packages/rum-react/src/domain/performance/index.ts b/packages/rum-react/src/domain/performance/index.ts new file mode 100644 index 0000000000..1eaa6ea7fb --- /dev/null +++ b/packages/rum-react/src/domain/performance/index.ts @@ -0,0 +1 @@ +export { ReactComponentTracker } from './reactComponentTracker' diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx new file mode 100644 index 0000000000..8807f7e5a9 --- /dev/null +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import { getTimer } from './getTimer' +import { addDurationVital } from './addDurationVital' + +export const ReactComponentTracker = ({ + name: componentName, + children, +}: { + name: string + context?: object + children?: React.ReactNode + burstDebounce?: number +}) => { + const isFirstRender = React.useRef(true) + + const renderTimer = getTimer() + const effectTimer = getTimer() + const layoutEffectTimer = getTimer() + + const onEffectEnd = () => { + const renderDuration = renderTimer.getDuration() + const effectDuration = effectTimer.getDuration() + const layoutEffectDuration = layoutEffectTimer.getDuration() + + const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration + + addDurationVital(`${componentName}`, { + startTime: renderTimer.getStartTime(), + duration: totalRenderTime, + context: { + isFirstRender: isFirstRender.current, + renderPhaseDuration: renderDuration, + effectPhaseDuration: effectDuration, + layoutEffectPhaseDuration: layoutEffectDuration, + componentName, + framework: 'react', + }, + }) + + /** + * Send a custom vital tracking this duration + */ + + isFirstRender.current = false + } + + /** + * In react, children are rendered sequentially + * in the order they are defined. that's why we + * can measure perf timings of a component by + * starting recordings in the component above + * and stopping them in the component below. + */ + return ( + <> + { + renderTimer.startTimer() + }} + onLayoutEffect={() => { + layoutEffectTimer.startTimer() + }} + onEffect={() => { + effectTimer.startTimer() + }} + /> + {children} + { + renderTimer.stopTimer() + }} + onLayoutEffect={() => { + layoutEffectTimer.stopTimer() + }} + onEffect={() => { + effectTimer.stopTimer() + onEffectEnd() + }} + /> + + ) +} + +function LifeCycle({ + onRender, + onLayoutEffect, + onEffect, +}: { + onRender: () => void + onLayoutEffect: () => void + onEffect: () => void +}) { + onRender() + React.useLayoutEffect(onLayoutEffect) + React.useEffect(onEffect) + return null +} diff --git a/packages/rum-react/src/entries/main.ts b/packages/rum-react/src/entries/main.ts index dd221ccccd..45bfaed3f6 100644 --- a/packages/rum-react/src/entries/main.ts +++ b/packages/rum-react/src/entries/main.ts @@ -1,2 +1,3 @@ export { ErrorBoundary, addReactError } from '../domain/error' export { reactPlugin } from '../domain/reactPlugin' +export { ReactComponentTracker } from '../domain/performance' diff --git a/sandbox/react-app/main.tsx b/sandbox/react-app/main.tsx index 8824d49ff4..363146e3c1 100644 --- a/sandbox/react-app/main.tsx +++ b/sandbox/react-app/main.tsx @@ -3,7 +3,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 { reactPlugin, ErrorBoundary } from '@datadog/browser-rum-react' +import { reactPlugin, ErrorBoundary, ReactComponentTracker } from '@datadog/browser-rum-react' datadogRum.init({ applicationId: 'xxx', @@ -66,7 +66,11 @@ function HomePage() { function UserPage() { const { id } = useParams() - return

User {id}

+ return ( + +

User {id}

+
+ ) } function WildCardPage() { diff --git a/webpack.base.js b/webpack.base.js index a3b3396189..14879cc784 100644 --- a/webpack.base.js +++ b/webpack.base.js @@ -35,7 +35,7 @@ module.exports = ({ entry, mode, filename, types, keepBuildEnvVariables, plugins }, resolve: { - extensions: ['.ts', '.js'], + extensions: ['.ts', '.js', '.tsx'], plugins: [new TsconfigPathsPlugin({ configFile: tsconfigPath })], alias: { // The default "pako.esm.js" build is not transpiled to es5 diff --git a/yarn.lock b/yarn.lock index e9a29023af..e206eb85e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -353,9 +353,11 @@ __metadata: "@datadog/browser-rum-core": "npm:5.34.1" "@types/react": "npm:18.3.17" "@types/react-dom": "npm:18.3.5" + "@types/uuid": "npm:^10" react: "npm:18.3.1" react-dom: "npm:18.3.1" react-router-dom: "npm:6.28.0" + uuid: "npm:10.0.0" peerDependencies: react: 18 react-router-dom: 6 @@ -2041,6 +2043,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^10": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: 10c0/9a1404bf287164481cb9b97f6bb638f78f955be57c40c6513b7655160beb29df6f84c915aaf4089a1559c216557dc4d2f79b48d978742d3ae10b937420ddac60 + languageName: node + linkType: hard + "@types/which@npm:^2.0.1": version: 2.0.2 resolution: "@types/which@npm:2.0.2" @@ -13570,21 +13579,21 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" +"uuid@npm:10.0.0, uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" bin: uuid: dist/bin/uuid - checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b + checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe languageName: node linkType: hard -"uuid@npm:^10.0.0": - version: 10.0.0 - resolution: "uuid@npm:10.0.0" +"uuid@npm:9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" bin: uuid: dist/bin/uuid - checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe + checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b languageName: node linkType: hard