From 5407cc36758e9c2c36e5919009eeb8c3a7f420a1 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Mon, 21 Oct 2024 10:37:50 +0200 Subject: [PATCH 01/22] POC collecting render duration for React components --- .../domain/performance/addDurationVital.ts | 8 + .../src/domain/performance/getTimer.spec.ts | 13 ++ .../src/domain/performance/getTimer.ts | 26 +++ .../rum-react/src/domain/performance/index.ts | 1 + .../performance/reactComponentTracker.tsx | 167 ++++++++++++++++++ packages/rum-react/src/entries/main.ts | 1 + 6 files changed, 216 insertions(+) create mode 100644 packages/rum-react/src/domain/performance/addDurationVital.ts create mode 100644 packages/rum-react/src/domain/performance/getTimer.spec.ts create mode 100644 packages/rum-react/src/domain/performance/getTimer.ts create mode 100644 packages/rum-react/src/domain/performance/index.ts create mode 100644 packages/rum-react/src/domain/performance/reactComponentTracker.tsx 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..5bc32ef6da --- /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('test') + + 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..43acbd1e9f --- /dev/null +++ b/packages/rum-react/src/domain/performance/getTimer.ts @@ -0,0 +1,26 @@ +import { generateUUID } from '@datadog/browser-core' + +export function getTimer(name: string) { + const id = generateUUID() + let measure: PerformanceMeasure + let startTime: number + + function startTimer() { + const start = performance.mark(`${name}-${id}`) + startTime = start.startTime + } + + function stopTimer() { + measure = performance.measure(`measure-${name}-${id}`, `${name}-${id}`) + } + + function getDuration() { + return measure ? measure.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..8d29c62865 --- /dev/null +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -0,0 +1,167 @@ +import * as React from 'react' +import { getTimer } from './getTimer' +import { addDurationVital } from './addDurationVital' +// import { count } from './count' + +// const SAFETY_BURST_DEBOUNCE_DELAY = 50 // ms + +// const useReactMountTracker = ( +// _: string, +// context: object, +// { +// isEnabled = true, +// burstDebounce = 500, +// }: { +// isEnabled?: boolean +// burstDebounce?: number +// } +// ) => { +// // useUserActionCounts(componentName, id, isEnabled) + +// const burstTimeout = React.useRef | false>() +// const burstRenderCount = React.useRef(0) + +// // we do this to guard against sending too many metrics +// const debounceDelay = Math.max(burstDebounce, SAFETY_BURST_DEBOUNCE_DELAY) +// burstRenderCount.current += 1 +// if (burstTimeout.current) { +// clearTimeout(burstTimeout.current) +// } + +// burstTimeout.current = +// isEnabled && +// setTimeout(() => { +// burstTimeout.current = undefined +// // emit the amount of renders in a given burst +// count({ +// name: 'react.burst_renders', +// value: burstRenderCount.current, +// context: { +// ...context, +// debounce: burstDebounce, +// }, +// }) +// burstRenderCount.current = 0 +// }, debounceDelay) + +// React.useEffect(() => { +// if (isEnabled) { +// count({ name: 'react.mount', context }) +// } + +// return () => { +// if (burstTimeout.current) { +// clearTimeout(burstTimeout.current) +// } +// burstTimeout.current = undefined +// } +// // FIXME: Update the dependency list to be exhaustive, and delete this comment. +// }, []) +// } + +export const ReactComponentTracker = ({ + name: componentName, + // context: contextProp, + children, + // burstDebounce = 500, +}: { + name: string + context?: object + children?: React.ReactNode + burstDebounce?: number +}) => { + const isFirstRender = React.useRef(true) + + // const context = { + // component: componentName, + // isFirstRender: isFirstRender.current, + // ...contextProp, + // } + + // useReactMountRecorder(componentName, context, { + // burstDebounce, + // }) + + const renderTimer = getTimer('render') + const effectTimer = getTimer('effect') + const layoutEffectTimer = getTimer('layout effect') + + 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' From d1ca10e5698829902430aa8148cb130427d2d3b9 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Mon, 21 Oct 2024 10:51:08 +0200 Subject: [PATCH 02/22] Remove hard dependency on `@datadog/browser-rum-core` --- packages/rum-react/archive.tgz | Bin 0 -> 26249 bytes packages/rum-react/package.json | 4 ++- .../src/domain/performance/getTimer.ts | 4 +-- yarn.lock | 25 ++++++++++++------ 4 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 packages/rum-react/archive.tgz diff --git a/packages/rum-react/archive.tgz b/packages/rum-react/archive.tgz new file mode 100644 index 0000000000000000000000000000000000000000..012485c9a4e50ed94693a3276481e4e639118bfb GIT binary patch literal 26249 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 b@1m_W!zi%w-vN!604&d^C{L&-7^wdPOU8Kf literal 0 HcmV?d00001 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/getTimer.ts b/packages/rum-react/src/domain/performance/getTimer.ts index 43acbd1e9f..592114c763 100644 --- a/packages/rum-react/src/domain/performance/getTimer.ts +++ b/packages/rum-react/src/domain/performance/getTimer.ts @@ -1,7 +1,7 @@ -import { generateUUID } from '@datadog/browser-core' +import { v4 as uuid } from 'uuid' export function getTimer(name: string) { - const id = generateUUID() + const id = uuid() let measure: PerformanceMeasure let startTime: number 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 From 131ef7117569f3a9a4af1461967978f57a241f6c Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Mon, 21 Oct 2024 11:12:42 +0200 Subject: [PATCH 03/22] Add debug logs --- .../rum-react/src/domain/performance/reactComponentTracker.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 8d29c62865..962801fced 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -93,6 +93,8 @@ export const ReactComponentTracker = ({ const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration + console.log('>>>', { totalRenderTime, addDurationVital }) + addDurationVital(`${componentName}`, { startTime: renderTimer.getStartTime(), duration: totalRenderTime, From 51dec02733f5d335be1a140150b7ee38eab3ff70 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Mon, 21 Oct 2024 12:39:06 +0200 Subject: [PATCH 04/22] Fix startTime isssue + add some instrumentation in the React sandbox --- packages/rum-react/src/domain/performance/getTimer.ts | 2 +- packages/rum-react/src/domain/performance/index.ts | 3 ++- .../src/domain/performance/reactComponentTracker.tsx | 2 -- sandbox/react-app/main.tsx | 8 ++++++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/rum-react/src/domain/performance/getTimer.ts b/packages/rum-react/src/domain/performance/getTimer.ts index 592114c763..3865b9e62d 100644 --- a/packages/rum-react/src/domain/performance/getTimer.ts +++ b/packages/rum-react/src/domain/performance/getTimer.ts @@ -7,7 +7,7 @@ export function getTimer(name: string) { function startTimer() { const start = performance.mark(`${name}-${id}`) - startTime = start.startTime + startTime = performance.timeOrigin + start.startTime } function stopTimer() { diff --git a/packages/rum-react/src/domain/performance/index.ts b/packages/rum-react/src/domain/performance/index.ts index 1eaa6ea7fb..6b04f9aa4c 100644 --- a/packages/rum-react/src/domain/performance/index.ts +++ b/packages/rum-react/src/domain/performance/index.ts @@ -1 +1,2 @@ -export { ReactComponentTracker } from './reactComponentTracker' +// @ts-ignore need extension +export { ReactComponentTracker } from './reactComponentTracker.tsx' diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 962801fced..8d29c62865 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -93,8 +93,6 @@ export const ReactComponentTracker = ({ const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration - console.log('>>>', { totalRenderTime, addDurationVital }) - addDurationVital(`${componentName}`, { startTime: renderTimer.getStartTime(), duration: totalRenderTime, 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() { From eebe16b18949c363c0cdc5441870095c3107c2f1 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Mon, 21 Oct 2024 12:46:53 +0200 Subject: [PATCH 05/22] Remove ext in import statement --- packages/rum-react/src/domain/performance/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rum-react/src/domain/performance/index.ts b/packages/rum-react/src/domain/performance/index.ts index 6b04f9aa4c..1eaa6ea7fb 100644 --- a/packages/rum-react/src/domain/performance/index.ts +++ b/packages/rum-react/src/domain/performance/index.ts @@ -1,2 +1 @@ -// @ts-ignore need extension -export { ReactComponentTracker } from './reactComponentTracker.tsx' +export { ReactComponentTracker } from './reactComponentTracker' From f9325aebb57c18a6ee8764e15f3094e1c1091216 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Tue, 22 Oct 2024 17:03:44 +0200 Subject: [PATCH 06/22] Cleanup --- .../src/domain/performance/getTimer.ts | 15 ++-- .../performance/reactComponentTracker.tsx | 76 +------------------ webpack.base.js | 16 ++-- yarn.lock | 23 ++---- 4 files changed, 25 insertions(+), 105 deletions(-) diff --git a/packages/rum-react/src/domain/performance/getTimer.ts b/packages/rum-react/src/domain/performance/getTimer.ts index 3865b9e62d..896f5055fa 100644 --- a/packages/rum-react/src/domain/performance/getTimer.ts +++ b/packages/rum-react/src/domain/performance/getTimer.ts @@ -1,21 +1,18 @@ -import { v4 as uuid } from 'uuid' - -export function getTimer(name: string) { - const id = uuid() - let measure: PerformanceMeasure +export function getTimer() { + let duration: number let startTime: number function startTimer() { - const start = performance.mark(`${name}-${id}`) - startTime = performance.timeOrigin + start.startTime + const start = performance.now() + startTime = performance.timeOrigin + start } function stopTimer() { - measure = performance.measure(`measure-${name}-${id}`, `${name}-${id}`) + duration = performance.timeOrigin + performance.now() - startTime } function getDuration() { - return measure ? measure.duration : 0 + return duration ? duration : 0 } function getStartTime() { diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 8d29c62865..8807f7e5a9 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -1,69 +1,10 @@ import * as React from 'react' import { getTimer } from './getTimer' import { addDurationVital } from './addDurationVital' -// import { count } from './count' - -// const SAFETY_BURST_DEBOUNCE_DELAY = 50 // ms - -// const useReactMountTracker = ( -// _: string, -// context: object, -// { -// isEnabled = true, -// burstDebounce = 500, -// }: { -// isEnabled?: boolean -// burstDebounce?: number -// } -// ) => { -// // useUserActionCounts(componentName, id, isEnabled) - -// const burstTimeout = React.useRef | false>() -// const burstRenderCount = React.useRef(0) - -// // we do this to guard against sending too many metrics -// const debounceDelay = Math.max(burstDebounce, SAFETY_BURST_DEBOUNCE_DELAY) -// burstRenderCount.current += 1 -// if (burstTimeout.current) { -// clearTimeout(burstTimeout.current) -// } - -// burstTimeout.current = -// isEnabled && -// setTimeout(() => { -// burstTimeout.current = undefined -// // emit the amount of renders in a given burst -// count({ -// name: 'react.burst_renders', -// value: burstRenderCount.current, -// context: { -// ...context, -// debounce: burstDebounce, -// }, -// }) -// burstRenderCount.current = 0 -// }, debounceDelay) - -// React.useEffect(() => { -// if (isEnabled) { -// count({ name: 'react.mount', context }) -// } - -// return () => { -// if (burstTimeout.current) { -// clearTimeout(burstTimeout.current) -// } -// burstTimeout.current = undefined -// } -// // FIXME: Update the dependency list to be exhaustive, and delete this comment. -// }, []) -// } export const ReactComponentTracker = ({ name: componentName, - // context: contextProp, children, - // burstDebounce = 500, }: { name: string context?: object @@ -72,19 +13,9 @@ export const ReactComponentTracker = ({ }) => { const isFirstRender = React.useRef(true) - // const context = { - // component: componentName, - // isFirstRender: isFirstRender.current, - // ...contextProp, - // } - - // useReactMountRecorder(componentName, context, { - // burstDebounce, - // }) - - const renderTimer = getTimer('render') - const effectTimer = getTimer('effect') - const layoutEffectTimer = getTimer('layout effect') + const renderTimer = getTimer() + const effectTimer = getTimer() + const layoutEffectTimer = getTimer() const onEffectEnd = () => { const renderDuration = renderTimer.getDuration() @@ -164,4 +95,3 @@ function LifeCycle({ React.useEffect(onEffect) return null } -// diff --git a/webpack.base.js b/webpack.base.js index a3b3396189..b0b32a3414 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 @@ -55,14 +55,14 @@ module.exports = ({ entry, mode, filename, types, keepBuildEnvVariables, plugins new webpack.SourceMapDevToolPlugin( mode === 'development' ? // Use an inline source map during development (default options) - {} + {} : // When bundling for release, produce a source map file so it can be used for source code integration, - // but don't append the source map comment to bundles as we don't upload the source map to - // the CDN (yet). - { - filename: '[file].map', - append: false, - } + // but don't append the source map comment to bundles as we don't upload the source map to + // the CDN (yet). + { + filename: '[file].map', + append: false, + } ), createDefinePlugin({ keepBuildEnvVariables }), ...(plugins || []), diff --git a/yarn.lock b/yarn.lock index e206eb85e9..5e89ab8c12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2043,13 +2043,6 @@ __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" @@ -13579,21 +13572,21 @@ __metadata: languageName: node linkType: hard -"uuid@npm:10.0.0, 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 -"uuid@npm:9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" +"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 From 19cb0a67d3703921438332e7ef6dc3116f1517a3 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Tue, 22 Oct 2024 17:23:52 +0200 Subject: [PATCH 07/22] Fix formatting issue --- webpack.base.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webpack.base.js b/webpack.base.js index b0b32a3414..14879cc784 100644 --- a/webpack.base.js +++ b/webpack.base.js @@ -55,14 +55,14 @@ module.exports = ({ entry, mode, filename, types, keepBuildEnvVariables, plugins new webpack.SourceMapDevToolPlugin( mode === 'development' ? // Use an inline source map during development (default options) - {} + {} : // When bundling for release, produce a source map file so it can be used for source code integration, - // but don't append the source map comment to bundles as we don't upload the source map to - // the CDN (yet). - { - filename: '[file].map', - append: false, - } + // but don't append the source map comment to bundles as we don't upload the source map to + // the CDN (yet). + { + filename: '[file].map', + append: false, + } ), createDefinePlugin({ keepBuildEnvVariables }), ...(plugins || []), From 718555e2e4e98e9ea6257d01b5576d16cf3792fd Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Tue, 22 Oct 2024 18:52:49 +0200 Subject: [PATCH 08/22] Fix type error in test --- packages/rum-react/src/domain/performance/getTimer.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum-react/src/domain/performance/getTimer.spec.ts b/packages/rum-react/src/domain/performance/getTimer.spec.ts index 5bc32ef6da..8546bbf2b6 100644 --- a/packages/rum-react/src/domain/performance/getTimer.spec.ts +++ b/packages/rum-react/src/domain/performance/getTimer.spec.ts @@ -2,7 +2,7 @@ import { getTimer } from './getTimer' describe('getTimer', () => { it('is able to measure time', () => { - const timer = getTimer('test') + const timer = getTimer() timer.startTimer() setTimeout(() => { From ea3b972409b40c7fdd7a192aabf38b0e536ee930 Mon Sep 17 00:00:00 2001 From: Nazim Saouli Date: Thu, 19 Dec 2024 15:34:18 +0100 Subject: [PATCH 09/22] Fix bad merge issue --- yarn.lock | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5e89ab8c12..e206eb85e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2043,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" @@ -13572,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 From 53727a6e82069e1be79960d6d472cfe31a2f1e55 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Thu, 16 Jan 2025 13:25:40 +0100 Subject: [PATCH 10/22] update naming/unit test --- .../src/domain/performance/getTimer.spec.ts | 13 ----- .../rum-react/src/domain/performance/index.ts | 2 +- .../reactComponentTracker.spec.tsx | 29 ++++++++++ .../performance/reactComponentTracker.tsx | 57 +++++++------------ .../src/domain/performance/timer.spec.ts | 15 +++++ .../performance/{getTimer.ts => timer.ts} | 10 ++-- packages/rum-react/src/entries/main.ts | 2 +- 7 files changed, 73 insertions(+), 55 deletions(-) delete mode 100644 packages/rum-react/src/domain/performance/getTimer.spec.ts create mode 100644 packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx create mode 100644 packages/rum-react/src/domain/performance/timer.spec.ts rename packages/rum-react/src/domain/performance/{getTimer.ts => timer.ts} (73%) diff --git a/packages/rum-react/src/domain/performance/getTimer.spec.ts b/packages/rum-react/src/domain/performance/getTimer.spec.ts deleted file mode 100644 index 8546bbf2b6..0000000000 --- a/packages/rum-react/src/domain/performance/getTimer.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/index.ts b/packages/rum-react/src/domain/performance/index.ts index 1eaa6ea7fb..15d60de1f8 100644 --- a/packages/rum-react/src/domain/performance/index.ts +++ b/packages/rum-react/src/domain/performance/index.ts @@ -1 +1 @@ -export { ReactComponentTracker } from './reactComponentTracker' +export { UNSTABLE_ReactComponentTracker } from './reactComponentTracker' diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx new file mode 100644 index 0000000000..fc368cbe90 --- /dev/null +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { appendComponent } from '../../../test/appendComponent' +import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +// eslint-disable-next-line +import { UNSTABLE_ReactComponentTracker } from './reactComponentTracker' + + +describe('UNSTABLE_ReactComponentTracker (simple)', () => { + + it('calls addDurationVital after the component rendering', () => { + const addDurationVitalSpy = jasmine.createSpy() + initializeReactPlugin({ + publicApi: { + addDurationVital: addDurationVitalSpy, + }, + }) + appendComponent( + // eslint-disable-next-line + +
child
+
+ ) + + expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) + const [componentName, payload] = addDurationVitalSpy.calls.mostRecent().args + expect(componentName).toBe('MyTestComponent') + expect(payload.context.s_first_render).toBe(true) + }) +}) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 8807f7e5a9..da0b2dceba 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -1,8 +1,9 @@ import * as React from 'react' -import { getTimer } from './getTimer' +import { createTimer } from './timer' import { addDurationVital } from './addDurationVital' -export const ReactComponentTracker = ({ +// eslint-disable-next-line +export const UNSTABLE_ReactComponentTracker = ({ name: componentName, children, }: { @@ -13,34 +14,30 @@ export const ReactComponentTracker = ({ }) => { const isFirstRender = React.useRef(true) - const renderTimer = getTimer() - const effectTimer = getTimer() - const layoutEffectTimer = getTimer() + const renderTimer = createTimer() + const effectTimer = createTimer() + const layoutEffectTimer = createTimer() const onEffectEnd = () => { - const renderDuration = renderTimer.getDuration() - const effectDuration = effectTimer.getDuration() - const layoutEffectDuration = layoutEffectTimer.getDuration() - + const renderDuration = renderTimer.getDuration() ?? 0 + const effectDuration = effectTimer.getDuration() ?? 0 + const layoutEffectDuration = layoutEffectTimer.getDuration() ?? 0 + const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration - addDurationVital(`${componentName}`, { - startTime: renderTimer.getStartTime(), + addDurationVital(componentName, { + startTime: renderTimer.getStartTime() ?? 0, duration: totalRenderTime, context: { - isFirstRender: isFirstRender.current, - renderPhaseDuration: renderDuration, - effectPhaseDuration: effectDuration, - layoutEffectPhaseDuration: layoutEffectDuration, - componentName, + s_first_render: isFirstRender.current, + render_phase_duration: renderDuration, + effect_phase_duration: effectDuration, + layout_effect_phase_duration: layoutEffectDuration, + component_name: componentName, framework: 'react', }, }) - /** - * Send a custom vital tracking this duration - */ - isFirstRender.current = false } @@ -54,24 +51,14 @@ export const ReactComponentTracker = ({ return ( <> { - renderTimer.startTimer() - }} - onLayoutEffect={() => { - layoutEffectTimer.startTimer() - }} - onEffect={() => { - effectTimer.startTimer() - }} + onRender={renderTimer.startTimer} + onLayoutEffect={layoutEffectTimer.startTimer} + onEffect={effectTimer.startTimer} /> {children} { - renderTimer.stopTimer() - }} - onLayoutEffect={() => { - layoutEffectTimer.stopTimer() - }} + onRender={renderTimer.stopTimer} + onLayoutEffect={layoutEffectTimer.stopTimer} onEffect={() => { effectTimer.stopTimer() onEffectEnd() diff --git a/packages/rum-react/src/domain/performance/timer.spec.ts b/packages/rum-react/src/domain/performance/timer.spec.ts new file mode 100644 index 0000000000..5bf5b834a3 --- /dev/null +++ b/packages/rum-react/src/domain/performance/timer.spec.ts @@ -0,0 +1,15 @@ +import { createTimer } from './timer' +import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' + +describe('getTimer', () => { + it('is able to measure time', () => { + const clock = mockClock() + registerCleanupTask(clock.cleanup) + + const timer = createTimer() + timer.startTimer() + clock.tick(1000) + timer.stopTimer() + expect(timer.getDuration()).toBe(1000) + }) +}) diff --git a/packages/rum-react/src/domain/performance/getTimer.ts b/packages/rum-react/src/domain/performance/timer.ts similarity index 73% rename from packages/rum-react/src/domain/performance/getTimer.ts rename to packages/rum-react/src/domain/performance/timer.ts index 896f5055fa..f87c785d19 100644 --- a/packages/rum-react/src/domain/performance/getTimer.ts +++ b/packages/rum-react/src/domain/performance/timer.ts @@ -1,6 +1,6 @@ -export function getTimer() { - let duration: number - let startTime: number +export function createTimer() { + let duration: number | undefined + let startTime: number | undefined function startTimer() { const start = performance.now() @@ -8,11 +8,11 @@ export function getTimer() { } function stopTimer() { - duration = performance.timeOrigin + performance.now() - startTime + duration = performance.timeOrigin + performance.now() - startTime! } function getDuration() { - return duration ? duration : 0 + return duration } function getStartTime() { diff --git a/packages/rum-react/src/entries/main.ts b/packages/rum-react/src/entries/main.ts index 45bfaed3f6..b2a5a9334d 100644 --- a/packages/rum-react/src/entries/main.ts +++ b/packages/rum-react/src/entries/main.ts @@ -1,3 +1,3 @@ export { ErrorBoundary, addReactError } from '../domain/error' export { reactPlugin } from '../domain/reactPlugin' -export { ReactComponentTracker } from '../domain/performance' +export { UNSTABLE_ReactComponentTracker } from '../domain/performance' From fd3e7512d9b1340ff0e9135f14226b8ffd412106 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Thu, 16 Jan 2025 13:32:46 +0100 Subject: [PATCH 11/22] prettier --- packages/rum-react/src/domain/performance/index.ts | 1 + .../performance/reactComponentTracker.spec.tsx | 14 ++++++-------- .../domain/performance/reactComponentTracker.tsx | 4 ++-- .../rum-react/src/domain/performance/timer.spec.ts | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/rum-react/src/domain/performance/index.ts b/packages/rum-react/src/domain/performance/index.ts index 15d60de1f8..06fdb9e674 100644 --- a/packages/rum-react/src/domain/performance/index.ts +++ b/packages/rum-react/src/domain/performance/index.ts @@ -1 +1,2 @@ +// eslint-disable-next-line camelcase export { UNSTABLE_ReactComponentTracker } from './reactComponentTracker' diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index fc368cbe90..cb2e80e204 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -1,21 +1,19 @@ import React from 'react' import { appendComponent } from '../../../test/appendComponent' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' -// eslint-disable-next-line +// eslint-disable-next-line camelcase import { UNSTABLE_ReactComponentTracker } from './reactComponentTracker' - -describe('UNSTABLE_ReactComponentTracker (simple)', () => { - +describe('UNSTABLE_ReactComponentTracker', () => { it('calls addDurationVital after the component rendering', () => { const addDurationVitalSpy = jasmine.createSpy() initializeReactPlugin({ - publicApi: { - addDurationVital: addDurationVitalSpy, - }, + publicApi: { + addDurationVital: addDurationVitalSpy, + }, }) appendComponent( - // eslint-disable-next-line + // eslint-disable-next-line camelcase
child
diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index da0b2dceba..6445d78637 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -19,10 +19,10 @@ export const UNSTABLE_ReactComponentTracker = ({ const layoutEffectTimer = createTimer() const onEffectEnd = () => { - const renderDuration = renderTimer.getDuration() ?? 0 + const renderDuration = renderTimer.getDuration() ?? 0 const effectDuration = effectTimer.getDuration() ?? 0 const layoutEffectDuration = layoutEffectTimer.getDuration() ?? 0 - + const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration addDurationVital(componentName, { diff --git a/packages/rum-react/src/domain/performance/timer.spec.ts b/packages/rum-react/src/domain/performance/timer.spec.ts index 5bf5b834a3..e52e16c5f1 100644 --- a/packages/rum-react/src/domain/performance/timer.spec.ts +++ b/packages/rum-react/src/domain/performance/timer.spec.ts @@ -1,5 +1,5 @@ -import { createTimer } from './timer' import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' +import { createTimer } from './timer' describe('getTimer', () => { it('is able to measure time', () => { From a235e655ca52a87c658f253a3cec9f9c970e08c0 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Thu, 16 Jan 2025 17:14:30 +0100 Subject: [PATCH 12/22] naming / vitalObject / delete archive file --- packages/rum-react/archive.tgz | Bin 26249 -> 0 bytes packages/rum-react/package.json | 4 +--- .../performance/reactComponentTracker.spec.tsx | 2 +- .../performance/reactComponentTracker.tsx | 6 +++--- .../src/domain/performance/timer.spec.ts | 2 +- packages/rum-react/src/entries/main.ts | 1 + 6 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 packages/rum-react/archive.tgz diff --git a/packages/rum-react/archive.tgz b/packages/rum-react/archive.tgz deleted file mode 100644 index 012485c9a4e50ed94693a3276481e4e639118bfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26249 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 b@1m_W!zi%w-vN!604&d^C{L&-7^wdPOU8Kf diff --git a/packages/rum-react/package.json b/packages/rum-react/package.json index 49024c3323..1ccb3b3cab 100644 --- a/packages/rum-react/package.json +++ b/packages/rum-react/package.json @@ -13,8 +13,7 @@ }, "dependencies": { "@datadog/browser-core": "5.34.1", - "@datadog/browser-rum-core": "5.34.1", - "uuid": "10.0.0" + "@datadog/browser-rum-core": "5.34.1" }, "peerDependencies": { "react": "18", @@ -37,7 +36,6 @@ "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/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index cb2e80e204..cb7ee979db 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -22,6 +22,6 @@ describe('UNSTABLE_ReactComponentTracker', () => { expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) const [componentName, payload] = addDurationVitalSpy.calls.mostRecent().args expect(componentName).toBe('MyTestComponent') - expect(payload.context.s_first_render).toBe(true) + expect(payload.context.is_first_render).toBe(true) }) }) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 6445d78637..ff7164d2d2 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -25,15 +25,15 @@ export const UNSTABLE_ReactComponentTracker = ({ const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration - addDurationVital(componentName, { + addDurationVital('React Component Render', { + description: componentName, startTime: renderTimer.getStartTime() ?? 0, duration: totalRenderTime, context: { - s_first_render: isFirstRender.current, + is_first_render: isFirstRender.current, render_phase_duration: renderDuration, effect_phase_duration: effectDuration, layout_effect_phase_duration: layoutEffectDuration, - component_name: componentName, framework: 'react', }, }) diff --git a/packages/rum-react/src/domain/performance/timer.spec.ts b/packages/rum-react/src/domain/performance/timer.spec.ts index e52e16c5f1..a537d3c32c 100644 --- a/packages/rum-react/src/domain/performance/timer.spec.ts +++ b/packages/rum-react/src/domain/performance/timer.spec.ts @@ -1,7 +1,7 @@ import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' import { createTimer } from './timer' -describe('getTimer', () => { +describe('createTimer', () => { it('is able to measure time', () => { const clock = mockClock() registerCleanupTask(clock.cleanup) diff --git a/packages/rum-react/src/entries/main.ts b/packages/rum-react/src/entries/main.ts index b2a5a9334d..b188d536c4 100644 --- a/packages/rum-react/src/entries/main.ts +++ b/packages/rum-react/src/entries/main.ts @@ -1,3 +1,4 @@ export { ErrorBoundary, addReactError } from '../domain/error' export { reactPlugin } from '../domain/reactPlugin' +// eslint-disable-next-line camelcase export { UNSTABLE_ReactComponentTracker } from '../domain/performance' From b1336131e9118e5e207c1aed59c32b553c987604 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Thu, 16 Jan 2025 17:18:26 +0100 Subject: [PATCH 13/22] update sandbox --- sandbox/react-app/main.tsx | 6 +++--- yarn.lock | 25 ++++++++----------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/sandbox/react-app/main.tsx b/sandbox/react-app/main.tsx index 363146e3c1..07188e9b33 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, ReactComponentTracker } from '@datadog/browser-rum-react' +import { reactPlugin, ErrorBoundary, UNSTABLE_ReactComponentTracker } from '@datadog/browser-rum-react' datadogRum.init({ applicationId: 'xxx', @@ -67,9 +67,9 @@ function HomePage() { function UserPage() { const { id } = useParams() return ( - +

User {id}

-
+ ) } diff --git a/yarn.lock b/yarn.lock index e206eb85e9..e9a29023af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -353,11 +353,9 @@ __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 @@ -2043,13 +2041,6 @@ __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" @@ -13579,21 +13570,21 @@ __metadata: languageName: node linkType: hard -"uuid@npm:10.0.0, 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 -"uuid@npm:9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" +"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 From aec4b0eed2a1070b69c6505ee45c5c8009eef9ff Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Thu, 16 Jan 2025 17:28:50 +0100 Subject: [PATCH 14/22] update vital name --- .../src/domain/performance/reactComponentTracker.spec.tsx | 4 ++-- .../src/domain/performance/reactComponentTracker.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index cb7ee979db..c8c4d1d4ae 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -14,14 +14,14 @@ describe('UNSTABLE_ReactComponentTracker', () => { }) appendComponent( // eslint-disable-next-line camelcase - +
child
) expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) const [componentName, payload] = addDurationVitalSpy.calls.mostRecent().args - expect(componentName).toBe('MyTestComponent') + expect(componentName).toBe('reactComponentRender') expect(payload.context.is_first_render).toBe(true) }) }) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index ff7164d2d2..80cadefcb5 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -25,7 +25,7 @@ export const UNSTABLE_ReactComponentTracker = ({ const totalRenderTime = renderDuration + effectDuration + layoutEffectDuration - addDurationVital('React Component Render', { + addDurationVital('reactComponentRender', { description: componentName, startTime: renderTimer.getStartTime() ?? 0, duration: totalRenderTime, From 821a48a84e9a7cf74704392425867819a2207892 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Fri, 17 Jan 2025 10:27:45 +0100 Subject: [PATCH 15/22] add context to vital object --- .../src/domain/performance/reactComponentTracker.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 80cadefcb5..90e0474590 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -5,12 +5,12 @@ import { addDurationVital } from './addDurationVital' // eslint-disable-next-line export const UNSTABLE_ReactComponentTracker = ({ name: componentName, + context, children, }: { name: string context?: object - children?: React.ReactNode - burstDebounce?: number + children?: React.ReactNode }) => { const isFirstRender = React.useRef(true) @@ -35,6 +35,7 @@ export const UNSTABLE_ReactComponentTracker = ({ effect_phase_duration: effectDuration, layout_effect_phase_duration: layoutEffectDuration, framework: 'react', + ...context, }, }) From f32a7e3fc9f3dca1966887f84bfc009c1836a19c Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Fri, 17 Jan 2025 10:31:58 +0100 Subject: [PATCH 16/22] format --- .../rum-react/src/domain/performance/reactComponentTracker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 90e0474590..1d512920bf 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -10,7 +10,7 @@ export const UNSTABLE_ReactComponentTracker = ({ }: { name: string context?: object - children?: React.ReactNode + children?: React.ReactNode }) => { const isFirstRender = React.useRef(true) From 60e137da59426775ca06a36b968bc927ee39b182 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Mon, 20 Jan 2025 11:00:33 +0100 Subject: [PATCH 17/22] add unit test for framework value --- .../reactComponentTracker.spec.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index c8c4d1d4ae..dcdb589618 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -5,7 +5,7 @@ import { initializeReactPlugin } from '../../../test/initializeReactPlugin' import { UNSTABLE_ReactComponentTracker } from './reactComponentTracker' describe('UNSTABLE_ReactComponentTracker', () => { - it('calls addDurationVital after the component rendering', () => { + it('should call addDurationVital after the component rendering', () => { const addDurationVitalSpy = jasmine.createSpy() initializeReactPlugin({ publicApi: { @@ -24,4 +24,25 @@ describe('UNSTABLE_ReactComponentTracker', () => { expect(componentName).toBe('reactComponentRender') expect(payload.context.is_first_render).toBe(true) }) + + it('should overwrite the framework value if specified', () => { + const addDurationVitalSpy = jasmine.createSpy() + initializeReactPlugin({ + publicApi: { + addDurationVital: addDurationVitalSpy, + }, + }) + + appendComponent( + // eslint-disable-next-line camelcase + +
child
+
+ ) + + expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) + const [, payload] = addDurationVitalSpy.calls.mostRecent().args + expect(payload.context.is_first_render).toBe(true) + expect(payload.context.framework).toBe('angular') + }) }) From 4801b505888a4406ae021d7a95cce853af2d2e37 Mon Sep 17 00:00:00 2001 From: "roman.gaignault" Date: Mon, 20 Jan 2025 12:26:55 +0100 Subject: [PATCH 18/22] remove context prop --- .../reactComponentTracker.spec.tsx | 21 ------------------- .../performance/reactComponentTracker.tsx | 3 --- 2 files changed, 24 deletions(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index dcdb589618..526aec96e4 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -24,25 +24,4 @@ describe('UNSTABLE_ReactComponentTracker', () => { expect(componentName).toBe('reactComponentRender') expect(payload.context.is_first_render).toBe(true) }) - - it('should overwrite the framework value if specified', () => { - const addDurationVitalSpy = jasmine.createSpy() - initializeReactPlugin({ - publicApi: { - addDurationVital: addDurationVitalSpy, - }, - }) - - appendComponent( - // eslint-disable-next-line camelcase - -
child
-
- ) - - expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) - const [, payload] = addDurationVitalSpy.calls.mostRecent().args - expect(payload.context.is_first_render).toBe(true) - expect(payload.context.framework).toBe('angular') - }) }) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index 1d512920bf..edb3ead47f 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -5,11 +5,9 @@ import { addDurationVital } from './addDurationVital' // eslint-disable-next-line export const UNSTABLE_ReactComponentTracker = ({ name: componentName, - context, children, }: { name: string - context?: object children?: React.ReactNode }) => { const isFirstRender = React.useRef(true) @@ -35,7 +33,6 @@ export const UNSTABLE_ReactComponentTracker = ({ effect_phase_duration: effectDuration, layout_effect_phase_duration: layoutEffectDuration, framework: 'react', - ...context, }, }) From 2f5fbd3c79801fa77732337926ee492547ff9b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Tue, 21 Jan 2025 12:26:42 +0100 Subject: [PATCH 19/22] tweak timer implementation --- .../performance/reactComponentTracker.tsx | 2 +- .../rum-react/src/domain/performance/timer.ts | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index edb3ead47f..d9c8e40702 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -25,7 +25,7 @@ export const UNSTABLE_ReactComponentTracker = ({ addDurationVital('reactComponentRender', { description: componentName, - startTime: renderTimer.getStartTime() ?? 0, + startTime: renderTimer.getStartTime()!, // note: renderTimer should have been started at this point, so getStartTime should not return undefined duration: totalRenderTime, context: { is_first_render: isFirstRender.current, diff --git a/packages/rum-react/src/domain/performance/timer.ts b/packages/rum-react/src/domain/performance/timer.ts index f87c785d19..32fecfb0eb 100644 --- a/packages/rum-react/src/domain/performance/timer.ts +++ b/packages/rum-react/src/domain/performance/timer.ts @@ -1,23 +1,32 @@ +import type { Duration, RelativeTime, TimeStamp } from '@datadog/browser-core' +import { elapsed, relativeNow, timeStampNow } from '@datadog/browser-core' + export function createTimer() { - let duration: number | undefined - let startTime: number | undefined + let duration: Duration | undefined + let startTime: TimeStamp | undefined + let highPrecisionStartTime: RelativeTime | undefined - function startTimer() { - const start = performance.now() - startTime = performance.timeOrigin + start - } + return { + startTimer(this: void) { + // timeStampNow uses Date.now() internally, which is not high precision, but this is what is + // used for other events, so we use it here as well. + startTime = timeStampNow() - function stopTimer() { - duration = performance.timeOrigin + performance.now() - startTime! - } + // relativeNow uses performance.now() which is higher precision than Date.now(), so we use for + // the duration + highPrecisionStartTime = relativeNow() + }, - function getDuration() { - return duration - } + stopTimer(this: void) { + duration = elapsed(highPrecisionStartTime!, relativeNow()) + }, - function getStartTime() { - return startTime - } + getDuration(this: void) { + return duration + }, - return { startTimer, stopTimer, getDuration, getStartTime } + getStartTime(this: void) { + return startTime + }, + } } From cccd572a2bd74b19c6147b2bf0e9ba219b4e00c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Tue, 21 Jan 2025 12:27:00 +0100 Subject: [PATCH 20/22] improve tests --- .../reactComponentTracker.spec.tsx | 82 +++++++++++++++++-- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index 526aec96e4..c88c80fccf 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -1,10 +1,32 @@ -import React from 'react' +import React, { useEffect, useLayoutEffect } from 'react' +import { flushSync } from 'react-dom' import { appendComponent } from '../../../test/appendComponent' import { initializeReactPlugin } from '../../../test/initializeReactPlugin' +import type { Clock } from '../../../../core/test' +import { mockClock, registerCleanupTask } from '../../../../core/test' // eslint-disable-next-line camelcase import { UNSTABLE_ReactComponentTracker } from './reactComponentTracker' +const RENDER_DURATION = 100 +const EFFECT_DURATION = 101 +const LAYOUT_EFFECT_DURATION = 102 +const TOTAL_DURATION = RENDER_DURATION + EFFECT_DURATION + LAYOUT_EFFECT_DURATION + +function ChildComponent({ clock }: { clock: Clock }) { + clock.tick(RENDER_DURATION) + useEffect(() => clock.tick(EFFECT_DURATION)) + useLayoutEffect(() => clock.tick(LAYOUT_EFFECT_DURATION)) + return null +} + describe('UNSTABLE_ReactComponentTracker', () => { + let clock: Clock + + beforeEach(() => { + clock = mockClock() + registerCleanupTask(() => clock.cleanup()) + }) + it('should call addDurationVital after the component rendering', () => { const addDurationVitalSpy = jasmine.createSpy() initializeReactPlugin({ @@ -12,16 +34,64 @@ describe('UNSTABLE_ReactComponentTracker', () => { addDurationVital: addDurationVitalSpy, }, }) + appendComponent( // eslint-disable-next-line camelcase - -
child
+ + ) expect(addDurationVitalSpy).toHaveBeenCalledTimes(1) - const [componentName, payload] = addDurationVitalSpy.calls.mostRecent().args - expect(componentName).toBe('reactComponentRender') - expect(payload.context.is_first_render).toBe(true) + const [name, options] = addDurationVitalSpy.calls.mostRecent().args + expect(name).toBe('reactComponentRender') + expect(options).toEqual({ + description: 'ChildComponent', + startTime: clock.timeStamp(0), + duration: TOTAL_DURATION, + context: { + is_first_render: true, + render_phase_duration: RENDER_DURATION, + effect_phase_duration: EFFECT_DURATION, + layout_effect_phase_duration: LAYOUT_EFFECT_DURATION, + framework: 'react', + }, + }) + }) + + it('should call addDurationVital on rerender', () => { + const addDurationVitalSpy = jasmine.createSpy() + initializeReactPlugin({ + publicApi: { + addDurationVital: addDurationVitalSpy, + }, + }) + + let forceUpdate: () => void + + function App() { + const [, setState] = React.useState(0) + forceUpdate = () => setState((prev) => prev + 1) + return ( + <> + {/* eslint-disable-next-line camelcase */} + + + + + ) + } + + appendComponent() + + clock.tick(1) + + flushSync(() => { + forceUpdate!() + }) + + expect(addDurationVitalSpy).toHaveBeenCalledTimes(2) + const options = addDurationVitalSpy.calls.mostRecent().args[1] + expect(options.startTime).toBe(clock.timeStamp(TOTAL_DURATION + 1)) }) }) From 8c065629096496970586e1f5945be6aef3570cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Tue, 21 Jan 2025 14:13:03 +0100 Subject: [PATCH 21/22] =?UTF-8?q?=F0=9F=91=8C=20fix=20typecheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/rum-react/src/domain/performance/timer.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rum-react/src/domain/performance/timer.spec.ts b/packages/rum-react/src/domain/performance/timer.spec.ts index a537d3c32c..fd60e7f106 100644 --- a/packages/rum-react/src/domain/performance/timer.spec.ts +++ b/packages/rum-react/src/domain/performance/timer.spec.ts @@ -1,4 +1,5 @@ import { mockClock, registerCleanupTask } from '@datadog/browser-core/test' +import type { Duration } from '@datadog/browser-core' import { createTimer } from './timer' describe('createTimer', () => { @@ -10,6 +11,6 @@ describe('createTimer', () => { timer.startTimer() clock.tick(1000) timer.stopTimer() - expect(timer.getDuration()).toBe(1000) + expect(timer.getDuration()).toBe(1000 as Duration) }) }) From 5e57074a3e91c7bea848640b77d5cb022e23dce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Tue, 21 Jan 2025 17:44:14 +0100 Subject: [PATCH 22/22] =?UTF-8?q?=F0=9F=91=8C=20fix=20comment=20style=20+?= =?UTF-8?q?=20more=20exhaustive=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../performance/reactComponentTracker.spec.tsx | 13 ++++++++++++- .../domain/performance/reactComponentTracker.tsx | 10 +++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx index c88c80fccf..14f9df8245 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.spec.tsx @@ -92,6 +92,17 @@ describe('UNSTABLE_ReactComponentTracker', () => { expect(addDurationVitalSpy).toHaveBeenCalledTimes(2) const options = addDurationVitalSpy.calls.mostRecent().args[1] - expect(options.startTime).toBe(clock.timeStamp(TOTAL_DURATION + 1)) + expect(options).toEqual({ + description: 'ChildComponent', + startTime: clock.timeStamp(TOTAL_DURATION + 1), + duration: TOTAL_DURATION, + context: { + is_first_render: false, + render_phase_duration: RENDER_DURATION, + effect_phase_duration: EFFECT_DURATION, + layout_effect_phase_duration: LAYOUT_EFFECT_DURATION, + framework: 'react', + }, + }) }) }) diff --git a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx index d9c8e40702..a070e01ff9 100644 --- a/packages/rum-react/src/domain/performance/reactComponentTracker.tsx +++ b/packages/rum-react/src/domain/performance/reactComponentTracker.tsx @@ -39,13 +39,9 @@ export const UNSTABLE_ReactComponentTracker = ({ 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. - */ + // 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 ( <>