diff --git a/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx b/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx index 12472ff00..6359fd12b 100644 --- a/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx +++ b/packages/core/src/__tests__/rum/instrumentation/DdRumUserInteractionTracking.test.tsx @@ -543,6 +543,33 @@ describe('startTracking with injected jsx runtimes', () => { ); }); + it("M leave React's own factory alone W the app declared its runtime", async () => { + // A styling library that owns the element factory can route React.createElement + // through machinery of its own. Replacing it then feeds that machinery calls it was + // never written to receive - measured on a release build as the heap growing until + // Hermes aborted at startup. Once the app has told us where its JSX comes from, + // there is nothing to gain there and a crash to lose. + const before = React.createElement; + const runtime: Record = { + jsx: jest.fn(), + jsxs: jest.fn() + }; + + DdRumUserInteractionTracking.startTracking({}, [runtime]); + + expect(React.createElement).toBe(before); + // the declared runtime is still instrumented - that is what records the taps + expect(runtime.jsx).not.toBe(before); + }); + + it("M patch React's own factory W no runtime was declared", async () => { + const before = React.createElement; + + DdRumUserInteractionTracking.startTracking({}); + + expect(React.createElement).not.toBe(before); + }); + it('M restore the injected runtime W stopTracking is called', async () => { const jsx = jest.fn(); const jsxs = jest.fn(); diff --git a/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx b/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx index 8d3c4226f..71f86c1cd 100644 --- a/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx +++ b/packages/core/src/rum/instrumentation/interactionTracking/DdRumUserInteractionTracking.tsx @@ -243,17 +243,37 @@ export class DdRumUserInteractionTracking { options ); + // React's own factory is left alone once the app has told us which runtime it + // compiles to. Two reasons, and the second one is why this is not merely tidy. + // + // Under the automatic JSX transform such an app never calls React.createElement - + // its elements come from the runtime it declared - so patching it buys no action. + // + // And it is not free. A styling library that owns the element factory can route + // React.createElement through machinery of its own; replacing it then feeds that + // machinery calls it was never written to receive, including React's internal ones. + // Measured on a release build of a nativewind app: the heap grew without bound until + // Hermes aborted during startup. Skipping this patch there is what stops the crash - + // replacing the factory more carefully does not, which two earlier attempts at this + // established the hard way. + // + // `memo` stays patched either way: it only restores the original onPress for + // comparison, and was measured not to contribute to the crash. + const appDeclaredItsRuntime = jsxRuntimes.length > 0; + const originalCreateElement = reactModule['createElement']; - replaceProperty( - reactModule, - 'createElement', - (...args: Parameters): any => { - return this.patchCreateElementFunction( - originalCreateElement, - args - ); - } - ); + if (!appDeclaredItsRuntime) { + replaceProperty( + reactModule, + 'createElement', + (...args: Parameters): any => { + return this.patchCreateElementFunction( + originalCreateElement, + args + ); + } + ); + } const runtimes: JsxRuntimeModule[] = []; try {