Skip to content

Commit

Permalink
add customizeStack hook (#36819)
Browse files Browse the repository at this point in the history
Summary:
X-link: facebook/react-native#36819

Pull Request resolved: facebook#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: ee107fb7238dda19b5cdc292ade9c045fbefc530
  • Loading branch information
GijsWeterings authored and facebook-github-bot committed Apr 6, 2023
1 parent 29c77bf commit b67f26f
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Object {
"stickyWorkers": true,
"symbolicator": Object {
"customizeFrame": [Function],
"customizeStack": [Function],
},
"transformer": Object {
"allowOptionalDependencies": false,
Expand Down Expand Up @@ -284,6 +285,7 @@ Object {
"stickyWorkers": true,
"symbolicator": Object {
"customizeFrame": [Function],
"customizeStack": [Function],
},
"transformer": Object {
"allowOptionalDependencies": false,
Expand Down Expand Up @@ -460,6 +462,7 @@ Object {
"stickyWorkers": true,
"symbolicator": Object {
"customizeFrame": [Function],
"customizeStack": [Function],
},
"transformer": Object {
"allowOptionalDependencies": false,
Expand Down Expand Up @@ -636,6 +639,7 @@ Object {
"stickyWorkers": true,
"symbolicator": Object {
"customizeFrame": [Function],
"customizeStack": [Function],
},
"transformer": Object {
"allowOptionalDependencies": false,
Expand Down
6 changes: 6 additions & 0 deletions packages/metro-config/src/configTypes.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {CacheManagerFactory} from 'metro-file-map';
import type {CustomResolver} from 'metro-resolver';
import type {JsTransformerConfig} from 'metro-transform-worker';
import type {TransformResult} from 'metro/src/DeltaBundler';

import type {
DeltaResult,
Module,
Expand All @@ -25,6 +26,7 @@ import type {
} from 'metro/src/DeltaBundler/types.flow.js';
import type {Reporter} from 'metro/src/lib/reporting';
import type Server from 'metro/src/Server';
import type {IntermediateStackFrame} from '../../metro/src/Server/symbolicate';

export type ExtraTransformOptions = {
+preloadedModules?: {[path: string]: true, ...} | false,
Expand Down Expand Up @@ -183,6 +185,10 @@ type SymbolicatorConfigT = {
+methodName: ?string,
...
}) => ?{+collapse?: boolean} | Promise<?{+collapse?: boolean}>,
customizeStack: (
Array<IntermediateStackFrame>,
mixed,
) => Array<IntermediateStackFrame> | Promise<Array<IntermediateStackFrame>>,
};

type WatcherConfigT = {
Expand Down
1 change: 1 addition & 0 deletions packages/metro-config/src/defaults/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({

symbolicator: {
customizeFrame: () => {},
customizeStack: async (stack, _) => stack,
},

transformer: {
Expand Down
6 changes: 4 additions & 2 deletions packages/metro/src/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,8 @@ class Server {
debug('Start symbolication');
/* $FlowFixMe: where is `rawBody` defined? Is it added by the `connect` framework? */
const body = await req.rawBody;
const stack = JSON.parse(body).stack.map(frame => {
const parsedBody = JSON.parse(body);
const stack = parsedBody.stack.map(frame => {
if (frame.file && frame.file.includes('://')) {
return {
...frame,
Expand Down Expand Up @@ -1126,10 +1127,11 @@ class Server {
);

debug('Performing fast symbolication');
const symbolicatedStack = await await symbolicate(
const symbolicatedStack = await symbolicate(
stack,
zip(urls.values(), sourceMaps),
this._config,
parsedBody.extraData ?? {},
);

debug('Symbolication done');
Expand Down
41 changes: 38 additions & 3 deletions packages/metro/src/Server/__tests__/Server-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ describe('processRequest', () => {
}
return null;
},
customizeStack: (stack, extraData) => {
return stack.map(frame => {
return {
...frame,
...extraData,
wasCollapsedBefore: frame.collapse === true ? true : undefined,
};
});
},
},
});

Expand Down Expand Up @@ -953,7 +962,6 @@ describe('processRequest', () => {
},
"stack": Array [
Object {
"collapse": false,
"column": 0,
"customPropShouldBeLeftUnchanged": "foo",
"file": "/root/mybundle.js",
Expand Down Expand Up @@ -1116,7 +1124,6 @@ describe('processRequest', () => {
},
"stack": Array [
Object {
"collapse": false,
"column": 0,
"customPropShouldBeLeftUnchanged": "foo",
"file": "/root/mybundle.js",
Expand Down Expand Up @@ -1175,6 +1182,35 @@ describe('processRequest', () => {
});
});

it('should transform frames as specified in customizeStack', async () => {
// NOTE: See implementation of symbolicator.customizeStack above.

const response = await makeRequest('/symbolicate', {
rawBody: JSON.stringify({
stack: [
{
file: 'http://localhost:8081/mybundle.bundle?runModule=true',
lineNumber: 3,
column: 18,
},
],
extraData: {
customAnnotation: 'Baz',
},
}),
});

expect(response._getJSON()).toMatchObject({
stack: [
expect.objectContaining({
file: '/root/foo.js',
wasCollapsedBefore: true,
customAnnotation: 'Baz',
}),
],
});
});

it('should leave original file and position when cannot symbolicate', async () => {
const response = await makeRequest('/symbolicate', {
rawBody: JSON.stringify({
Expand All @@ -1195,7 +1231,6 @@ describe('processRequest', () => {
"codeFrame": null,
"stack": Array [
Object {
"collapse": false,
"column": 18,
"customPropShouldBeLeftUnchanged": "foo",
"file": "http://localhost:8081/mybundle.bundle?runModule=true",
Expand Down
49 changes: 40 additions & 9 deletions packages/metro/src/Server/symbolicate.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ export type StackFrameInput = {
+methodName: ?string,
...
};
export type StackFrameOutput = $ReadOnly<{
export type IntermediateStackFrame = {
...StackFrameInput,
+collapse: boolean,
collapse?: boolean,
...
};
export type StackFrameOutput = $ReadOnly<{
...IntermediateStackFrame,
...
}>;
type ExplodedSourceMapModule = $ElementType<ExplodedSourceMap, number>;
Expand Down Expand Up @@ -63,6 +67,7 @@ async function symbolicate(
stack: $ReadOnlyArray<StackFrameInput>,
maps: Iterable<[string, ExplodedSourceMap]>,
config: ConfigT,
extraData: mixed,
): Promise<$ReadOnlyArray<StackFrameOutput>> {
const mapsByUrl = new Map<?string, ExplodedSourceMap>();
for (const [url, map] of maps) {
Expand Down Expand Up @@ -157,10 +162,10 @@ async function symbolicate(
return null;
}

function symbolicateFrame(frame: StackFrameInput): StackFrameInput {
function symbolicateFrame(frame: StackFrameInput): IntermediateStackFrame {
const module = findModule(frame);
if (!module) {
return frame;
return {...frame};
}
if (!Array.isArray(module.map)) {
throw new Error(
Expand All @@ -169,7 +174,7 @@ async function symbolicate(
}
const originalPos = findOriginalPos(frame, module);
if (!originalPos) {
return frame;
return {...frame};
}
const methodName =
findFunctionName(originalPos, module) ?? frame.methodName;
Expand All @@ -182,15 +187,41 @@ async function symbolicate(
};
}

/**
* `customizeFrame` allows for custom modifications of the symbolicated frame in a stack.
* It can be used to collapse stack frames that are not relevant to users, pointing them
* to more relevant product code instead.
*
* An example usecase is a library throwing an error while sanitizing inputs from product code.
* In some cases, it's more useful to point the developer looking at the error towards the product code directly.
*/
async function customizeFrame(
frame: StackFrameInput,
): Promise<StackFrameOutput> {
frame: IntermediateStackFrame,
): Promise<IntermediateStackFrame> {
const customizations =
(await config.symbolicator.customizeFrame(frame)) || {};
return {...frame, collapse: false, ...customizations};
return {...frame, ...customizations};
}

return Promise.all(stack.map(symbolicateFrame).map(customizeFrame));
/**
* `customizeStack` allows for custom modifications of a symbolicated stack.
* Where `customizeFrame` operates on individual frames, this hook can process the entire stack in context.
*
* Note: `customizeStack` has access to an `extraData` object which can be used to attach metadata
* to the error coming in, to be used by the customizeStack hook.
*/
async function customizeStack(
symbolicatedStack: Array<IntermediateStackFrame>,
): Promise<Array<IntermediateStackFrame>> {
return await config.symbolicator.customizeStack(
symbolicatedStack,
extraData,
);
}

return Promise.all(stack.map(symbolicateFrame).map(customizeFrame)).then(
customizeStack,
);
}

module.exports = symbolicate;

0 comments on commit b67f26f

Please sign in to comment.