Skip to content

Commit bee7c05

Browse files
committed
ref(react): Check if React span is finished
1 parent ebeaffe commit bee7c05

File tree

4 files changed

+77
-257
lines changed

4 files changed

+77
-257
lines changed

packages/react/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
"@sentry/browser": "5.16.0-beta.5",
2020
"@sentry/types": "5.16.0-beta.5",
2121
"@sentry/utils": "^5.15.5",
22+
"hoist-non-react-statics": "^3.3.2",
2223
"tslib": "^1.9.3"
2324
},
2425
"peerDependencies": {
2526
"react": "^16.0.0",
2627
"react-dom": "^16.0.0"
2728
},
2829
"devDependencies": {
29-
"@testing-library/react": "^10.0.4",
30+
"@types/hoist-non-react-statics": "^3.3.1",
3031
"@types/react": "^16.9.35",
3132
"@types/react-test-renderer": "^16.9.2",
3233
"jest": "^24.7.1",

packages/react/src/profiler.tsx

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,57 @@
11
import { getCurrentHub } from '@sentry/browser';
22
import { Integration, IntegrationClass } from '@sentry/types';
33
import { logger } from '@sentry/utils';
4+
import * as hoistNonReactStatic from 'hoist-non-react-statics';
45
import * as React from 'react';
56

6-
export const DEFAULT_DURATION = 30000;
77
export const UNKNOWN_COMPONENT = 'unknown';
88

99
const TRACING_GETTER = ({
1010
id: 'Tracing',
1111
} as any) as IntegrationClass<Integration>;
1212

13-
const getInitActivity = (componentDisplayName: string, timeout: number): number | null => {
13+
/**
14+
*
15+
* Based on implementation from Preact:
16+
* https:github.com/preactjs/preact/blob/9a422017fec6dab287c77c3aef63c7b2fef0c7e1/hooks/src/index.js#L301-L313
17+
*
18+
* Schedule a callback to be invoked after the browser has a chance to paint a new frame.
19+
* Do this by combining requestAnimationFrame (rAF) + setTimeout to invoke a callback after
20+
* the next browser frame.
21+
*
22+
* Also, schedule a timeout in parallel to the the rAF to ensure the callback is invoked
23+
* even if RAF doesn't fire (for example if the browser tab is not visible)
24+
*
25+
* This is what we use to tell if a component activity has finished
26+
*
27+
*/
28+
function afterNextFrame(callback: Function): void {
29+
let timeout: number | undefined;
30+
let raf: number;
31+
32+
const done = () => {
33+
window.clearTimeout(timeout);
34+
window.cancelAnimationFrame(raf);
35+
window.setTimeout(callback);
36+
};
37+
38+
raf = window.requestAnimationFrame(done);
39+
timeout = window.setTimeout(done, 100);
40+
}
41+
42+
const getInitActivity = (componentDisplayName: string): number | null => {
1443
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
1544

1645
if (tracingIntegration !== null) {
1746
// tslint:disable-next-line:no-unsafe-any
18-
return (tracingIntegration as any).constructor.pushActivity(
19-
componentDisplayName,
20-
{
21-
data: {},
22-
description: `<${componentDisplayName}>`,
23-
op: 'react',
24-
},
25-
{
26-
autoPopAfter: timeout,
27-
},
28-
);
47+
const activity = (tracingIntegration as any).constructor.pushActivity(componentDisplayName, {
48+
data: {},
49+
description: `<${componentDisplayName}>`,
50+
op: 'react',
51+
});
52+
53+
// tslint:disable-next-line: no-unsafe-any
54+
return activity;
2955
}
3056

3157
logger.warn(`Unable to profile component ${componentDisplayName} due to invalid Tracing Integration`);
@@ -34,7 +60,6 @@ const getInitActivity = (componentDisplayName: string, timeout: number): number
3460

3561
interface ProfilerProps {
3662
componentDisplayName?: string;
37-
timeout?: number;
3863
}
3964

4065
interface ProfilerState {
@@ -45,15 +70,23 @@ class Profiler extends React.Component<ProfilerProps, ProfilerState> {
4570
public constructor(props: ProfilerProps) {
4671
super(props);
4772

48-
const { componentDisplayName = UNKNOWN_COMPONENT, timeout = DEFAULT_DURATION } = this.props;
73+
const { componentDisplayName = UNKNOWN_COMPONENT } = this.props;
4974

5075
this.state = {
51-
activity: getInitActivity(componentDisplayName, timeout),
76+
activity: getInitActivity(componentDisplayName),
5277
};
5378
}
5479

80+
public componentDidMount(): void {
81+
if (this.state.activity) {
82+
afterNextFrame(this.finishProfile);
83+
}
84+
}
85+
5586
public componentWillUnmount(): void {
56-
this.finishProfile();
87+
if (this.state.activity) {
88+
afterNextFrame(this.finishProfile);
89+
}
5790
}
5891

5992
public finishProfile = () => {
@@ -88,6 +121,9 @@ function withProfiler<P extends object>(
88121

89122
Wrapped.displayName = `profiler(${componentDisplayName})`;
90123

124+
// Copy over static methods from Wrapped component to Profiler HOC
125+
// See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
126+
hoistNonReactStatic(Wrapped, WrappedComponent);
91127
return Wrapped;
92128
}
93129

packages/react/test/profiler.test.tsx

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { create } from 'react-test-renderer';
33

4-
import { DEFAULT_DURATION, UNKNOWN_COMPONENT, withProfiler } from '../src/profiler';
4+
import { UNKNOWN_COMPONENT, withProfiler } from '../src/profiler';
55

66
const mockPushActivity = jest.fn().mockReturnValue(1);
77
const mockPopActivity = jest.fn();
@@ -57,35 +57,18 @@ describe('withProfiler', () => {
5757
expect(mockPushActivity).toHaveBeenCalledTimes(0);
5858
create(<ProfiledComponent />);
5959
expect(mockPushActivity).toHaveBeenCalledTimes(1);
60-
expect(mockPushActivity).toHaveBeenLastCalledWith(
61-
UNKNOWN_COMPONENT,
62-
{
63-
data: {},
64-
description: `<${UNKNOWN_COMPONENT}>`,
65-
op: 'react',
66-
},
67-
{ autoPopAfter: DEFAULT_DURATION },
68-
);
69-
});
70-
71-
it('is called with a custom timeout', () => {
72-
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>, { timeout: 32 });
73-
74-
create(<ProfiledComponent />);
75-
expect(mockPushActivity).toHaveBeenLastCalledWith(expect.any(String), expect.any(Object), {
76-
autoPopAfter: 32,
60+
expect(mockPushActivity).toHaveBeenLastCalledWith(UNKNOWN_COMPONENT, {
61+
data: {},
62+
description: `<${UNKNOWN_COMPONENT}>`,
63+
op: 'react',
7764
});
7865
});
7966

8067
it('is called with a custom displayName', () => {
8168
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>, { componentDisplayName: 'Test' });
8269

8370
create(<ProfiledComponent />);
84-
expect(mockPushActivity).toHaveBeenLastCalledWith(
85-
'Test',
86-
expect.objectContaining({ description: '<Test>' }),
87-
expect.any(Object),
88-
);
71+
expect(mockPushActivity).toHaveBeenLastCalledWith('Test', expect.objectContaining({ description: '<Test>' }));
8972
});
9073
});
9174
});

0 commit comments

Comments
 (0)