Skip to content

Commit ee70717

Browse files
authored
feat(react): Track duration of React component updates (#5531)
1 parent ac2dc12 commit ee70717

File tree

2 files changed

+24
-10
lines changed

2 files changed

+24
-10
lines changed

packages/react/src/profiler.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class Profiler extends React.Component<ProfilerProps> {
3434
* Made protected for the React Native SDK to access
3535
*/
3636
protected _mountSpan: Span | undefined = undefined;
37+
/**
38+
* The span that represents the duration of time between shouldComponentUpdate and componentDidUpdate
39+
*/
40+
protected _updateSpan: Span | undefined = undefined;
3741

3842
// eslint-disable-next-line @typescript-eslint/member-ordering
3943
public static defaultProps: Partial<ProfilerProps> = {
@@ -66,29 +70,35 @@ class Profiler extends React.Component<ProfilerProps> {
6670
}
6771
}
6872

69-
public componentDidUpdate({ updateProps, includeUpdates = true }: ProfilerProps): void {
70-
// Only generate an update span if hasUpdateSpan is true, if there is a valid mountSpan,
73+
public shouldComponentUpdate({ updateProps, includeUpdates = true }: ProfilerProps): boolean {
74+
// Only generate an update span if includeUpdates is true, if there is a valid mountSpan,
7175
// and if the updateProps have changed. It is ok to not do a deep equality check here as it is expensive.
7276
// We are just trying to give baseline clues for further investigation.
7377
if (includeUpdates && this._mountSpan && updateProps !== this.props.updateProps) {
7478
// See what props haved changed between the previous props, and the current props. This is
7579
// set as data on the span. We just store the prop keys as the values could be potenially very large.
7680
const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]);
7781
if (changedProps.length > 0) {
78-
// The update span is a point in time span with 0 duration, just signifying that the component
79-
// has been updated.
8082
const now = timestampWithMs();
81-
this._mountSpan.startChild({
83+
this._updateSpan = this._mountSpan.startChild({
8284
data: {
8385
changedProps,
8486
},
8587
description: `<${this.props.name}>`,
86-
endTimestamp: now,
8788
op: REACT_UPDATE_OP,
8889
startTimestamp: now,
8990
});
9091
}
9192
}
93+
94+
return true;
95+
}
96+
97+
public componentDidUpdate(): void {
98+
if (this._updateSpan) {
99+
this._updateSpan.finish();
100+
this._updateSpan = undefined;
101+
}
92102
}
93103

94104
// If a component is unmounted, we can say it is no longer on the screen.

packages/react/test/profiler.test.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jest.mock('@sentry/browser', () => ({
3636

3737
beforeEach(() => {
3838
mockStartChild.mockClear();
39+
mockFinish.mockClear();
3940
activeTransaction = new MockSpan({ op: 'pageload' });
4041
});
4142

@@ -100,7 +101,9 @@ describe('withProfiler', () => {
100101
});
101102

102103
it('is not created if hasRenderSpan is false', () => {
103-
const ProfiledComponent = withProfiler(() => <h1>Testing</h1>, { includeRender: false });
104+
const ProfiledComponent = withProfiler(() => <h1>Testing</h1>, {
105+
includeRender: false,
106+
});
104107
expect(mockStartChild).toHaveBeenCalledTimes(0);
105108

106109
const component = render(<ProfiledComponent />);
@@ -115,32 +118,33 @@ describe('withProfiler', () => {
115118
const ProfiledComponent = withProfiler((props: { num: number }) => <div>{props.num}</div>);
116119
const { rerender } = render(<ProfiledComponent num={0} />);
117120
expect(mockStartChild).toHaveBeenCalledTimes(1);
121+
expect(mockFinish).toHaveBeenCalledTimes(1);
118122

119123
// Dispatch new props
120124
rerender(<ProfiledComponent num={1} />);
121125
expect(mockStartChild).toHaveBeenCalledTimes(2);
122126
expect(mockStartChild).toHaveBeenLastCalledWith({
123127
data: { changedProps: ['num'] },
124128
description: `<${UNKNOWN_COMPONENT}>`,
125-
endTimestamp: expect.any(Number),
126129
op: REACT_UPDATE_OP,
127130
startTimestamp: expect.any(Number),
128131
});
129-
132+
expect(mockFinish).toHaveBeenCalledTimes(2);
130133
// New props yet again
131134
rerender(<ProfiledComponent num={2} />);
132135
expect(mockStartChild).toHaveBeenCalledTimes(3);
133136
expect(mockStartChild).toHaveBeenLastCalledWith({
134137
data: { changedProps: ['num'] },
135138
description: `<${UNKNOWN_COMPONENT}>`,
136-
endTimestamp: expect.any(Number),
137139
op: REACT_UPDATE_OP,
138140
startTimestamp: expect.any(Number),
139141
});
142+
expect(mockFinish).toHaveBeenCalledTimes(3);
140143

141144
// Should not create spans if props haven't changed
142145
rerender(<ProfiledComponent num={2} />);
143146
expect(mockStartChild).toHaveBeenCalledTimes(3);
147+
expect(mockFinish).toHaveBeenCalledTimes(3);
144148
});
145149

146150
it('does not get created if hasUpdateSpan is false', () => {

0 commit comments

Comments
 (0)