From 03e78010aef3984287c34902df46268bbe9723e9 Mon Sep 17 00:00:00 2001 From: Gijs Weterings Date: Thu, 6 Apr 2023 11:43:23 -0700 Subject: [PATCH] add customizeStack hook (#36819) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36819 X-link: https://github.com/facebook/metro/pull/964 This diff creates a new hook to the Metro symbolicator. `customizeStack` aims to provide a whole stack modification hook on the output of the `/symbolicate` endpoint. The purpose of this hook is to be able to apply callsite-based modifications to the stack trace. One such example is user-facing frame skipping APIs like FBLogger internally. Consider the following API: ``` FBLogger('my_project') .blameToPreviousFile() .mustfix( 'This error should refer to the callsite of this method', ); ``` In this particular case, we'd want to skip all frames from the top that come from the same source file. To do that, we need knowledge of the entire symbolicated stack, neither a hook before symbolication nor an implementation in `symbolicator.customizeFrame` are sufficient to be able to apply this logic. This diff creates the new hook, which allows for mutations of the entire symbolicated stack via a `symbolicator.customizeStack` hook. The default implementation of this simply returns the same stack, but it can be wrapped similar to `symbolicator.customizeFrame`. To actually have information for this hook to act on, I've created the possibility to send additional data to the metro `/symbolicate` endpoint via an `extraData` object. This mirrors the `extraData` from https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Core/NativeExceptionsManager.js#L33, and I've wired up LogBox to send that object along with the symbolicate call. Changelog: [General][Added] - Added customizeStack hook to Metro's `/symbolicate` endpoint to allow custom frame skipping logic on a stack level. Reviewed By: motiz88 Differential Revision: D44257733 fbshipit-source-id: 05cd57f5917a1e97b0520e772692ce64029fbf8a --- .../Libraries/Core/Devtools/symbolicateStackTrace.js | 3 ++- packages/react-native/Libraries/LogBox/Data/LogBoxLog.js | 5 ++++- .../Libraries/LogBox/Data/LogBoxSymbolication.js | 7 +++++-- .../react-native/Libraries/LogBox/Data/parseLogBoxLog.js | 7 +++++++ .../__tests__/__snapshots__/LogBoxInspector-test.js.snap | 2 ++ .../__snapshots__/LogBoxInspectorContainer-test.js.snap | 4 ++++ .../__snapshots__/LogBoxNotificationContainer-test.js.snap | 2 ++ packages/react-native/types/modules/Devtools.d.ts | 1 + 8 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js b/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js index b45bc368d54f88..bdd93fc49a3f2a 100644 --- a/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js +++ b/packages/react-native/Libraries/Core/Devtools/symbolicateStackTrace.js @@ -31,6 +31,7 @@ export type SymbolicatedStackTrace = $ReadOnly<{ async function symbolicateStackTrace( stack: Array, + extraData?: mixed, ): Promise { const devServer = getDevServer(); if (!devServer.bundleLoadedFromServer) { @@ -41,7 +42,7 @@ async function symbolicateStackTrace( const fetch = global.fetch ?? require('../../Network/fetch'); const response = await fetch(devServer.url + 'symbolicate', { method: 'POST', - body: JSON.stringify({stack}), + body: JSON.stringify({stack, extraData}), }); return await response.json(); } diff --git a/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js b/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js index 4c98e048c0c746..a7f5a9fb5ba11c 100644 --- a/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js +++ b/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js @@ -31,6 +31,7 @@ export type LogBoxLogData = $ReadOnly<{| componentStack: ComponentStack, codeFrame?: ?CodeFrame, isComponentError: boolean, + extraData?: mixed, |}>; class LogBoxLog { @@ -43,6 +44,7 @@ class LogBoxLog { level: LogLevel; codeFrame: ?CodeFrame; isComponentError: boolean; + extraData: mixed | void; symbolicated: | $ReadOnly<{|error: null, stack: null, status: 'NONE'|}> | $ReadOnly<{|error: null, stack: null, status: 'PENDING'|}> @@ -62,6 +64,7 @@ class LogBoxLog { this.componentStack = data.componentStack; this.codeFrame = data.codeFrame; this.isComponentError = data.isComponentError; + this.extraData = data.extraData; this.count = 1; } @@ -91,7 +94,7 @@ class LogBoxLog { handleSymbolicate(callback?: (status: SymbolicationStatus) => void): void { if (this.symbolicated.status !== 'PENDING') { this.updateStatus(null, null, null, callback); - LogBoxSymbolication.symbolicate(this.stack).then( + LogBoxSymbolication.symbolicate(this.stack, this.extraData).then( data => { this.updateStatus(null, data?.stack, data?.codeFrame, callback); }, diff --git a/packages/react-native/Libraries/LogBox/Data/LogBoxSymbolication.js b/packages/react-native/Libraries/LogBox/Data/LogBoxSymbolication.js index 9e5aaa9ce56686..1b588c4f9050d6 100644 --- a/packages/react-native/Libraries/LogBox/Data/LogBoxSymbolication.js +++ b/packages/react-native/Libraries/LogBox/Data/LogBoxSymbolication.js @@ -51,10 +51,13 @@ export function deleteStack(stack: Stack): void { cache.delete(stack); } -export function symbolicate(stack: Stack): Promise { +export function symbolicate( + stack: Stack, + extraData?: mixed, +): Promise { let promise = cache.get(stack); if (promise == null) { - promise = symbolicateStackTrace(stack).then(sanitize); + promise = symbolicateStackTrace(stack, extraData).then(sanitize); cache.set(stack, promise); } diff --git a/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js b/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js index 7b91d697a880e2..b41bade7fd0be5 100644 --- a/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js +++ b/packages/react-native/Libraries/LogBox/Data/parseLogBoxLog.js @@ -211,6 +211,7 @@ export function parseLogBoxException( substitutions: [], }, category: `${fileName}-${row}-${column}`, + extraData: error.extraData, }; } @@ -238,6 +239,7 @@ export function parseLogBoxException( substitutions: [], }, category: `${fileName}-${row}-${column}`, + extraData: error.extraData, }; } @@ -261,6 +263,7 @@ export function parseLogBoxException( substitutions: [], }, category: `${fileName}-${1}-${1}`, + extraData: error.extraData, }; } @@ -275,6 +278,7 @@ export function parseLogBoxException( substitutions: [], }, category: message, + extraData: error.extraData, }; } @@ -286,6 +290,7 @@ export function parseLogBoxException( isComponentError: error.isComponentError, componentStack: componentStack != null ? parseComponentStack(componentStack) : [], + extraData: error.extraData, ...parseInterpolation([message]), }; } @@ -297,6 +302,7 @@ export function parseLogBoxException( stack: error.stack, isComponentError: error.isComponentError, componentStack: parseComponentStack(componentStack), + extraData: error.extraData, ...parseInterpolation([message]), }; } @@ -307,6 +313,7 @@ export function parseLogBoxException( level: 'error', stack: error.stack, isComponentError: error.isComponentError, + extraData: error.extraData, ...parseLogBoxLog([message]), }; } diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-test.js.snap b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-test.js.snap index 8259c01b67d4ff..0574a088ddf244 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-test.js.snap +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-test.js.snap @@ -22,6 +22,7 @@ exports[`LogBoxContainer should render fatal with selectedIndex 2 1`] = ` "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "fatal", "message": Object { @@ -71,6 +72,7 @@ exports[`LogBoxContainer should render warning with selectedIndex 0 1`] = ` "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "warn", "message": Object { diff --git a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxInspectorContainer-test.js.snap b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxInspectorContainer-test.js.snap index 99c5f74e76ad37..591971a62af560 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxInspectorContainer-test.js.snap +++ b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxInspectorContainer-test.js.snap @@ -28,6 +28,7 @@ exports[`LogBoxNotificationContainer should render both an error and warning not "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "warn", "message": Object { @@ -65,6 +66,7 @@ exports[`LogBoxNotificationContainer should render both an error and warning not "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "error", "message": Object { @@ -124,6 +126,7 @@ exports[`LogBoxNotificationContainer should render the latest error notification "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "error", "message": Object { @@ -175,6 +178,7 @@ exports[`LogBoxNotificationContainer should render the latest warning notificati "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "warn", "message": Object { diff --git a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap index c512ee317f257c..5402cb1504e05e 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap +++ b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap @@ -20,6 +20,7 @@ exports[`LogBoxNotificationContainer should render inspector with logs, even whe "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "warn", "message": Object { @@ -39,6 +40,7 @@ exports[`LogBoxNotificationContainer should render inspector with logs, even whe "codeFrame": undefined, "componentStack": Array [], "count": 1, + "extraData": undefined, "isComponentError": false, "level": "error", "message": Object { diff --git a/packages/react-native/types/modules/Devtools.d.ts b/packages/react-native/types/modules/Devtools.d.ts index 3a993cf7ab7b5b..d287d97c5a4257 100644 --- a/packages/react-native/types/modules/Devtools.d.ts +++ b/packages/react-native/types/modules/Devtools.d.ts @@ -27,5 +27,6 @@ declare module 'react-native/Libraries/Core/Devtools/symbolicateStackTrace' { export default function symbolicateStackTrace( stack: ReadonlyArray, + extraData?: any, ): Promise; }