Skip to content

Commit bb69254

Browse files
committed
fix(react): prevent duplicate spans in strict mode
1 parent 9b36c14 commit bb69254

File tree

1 file changed

+55
-10
lines changed

1 file changed

+55
-10
lines changed

packages/react/src/profiler.tsx

+55-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const TRACING_GETTER = ({
1010
id: 'Tracing',
1111
} as any) as IntegrationClass<Integration>;
1212

13+
// https://stackoverflow.com/questions/52702466/detect-react-reactdom-development-production-build
14+
function isReactInDevMode(): boolean {
15+
return '_self' in React.createElement('div');
16+
}
17+
1318
/**
1419
*
1520
* Based on implementation from Preact:
@@ -39,25 +44,65 @@ function afterNextFrame(callback: Function): void {
3944
timeout = window.setTimeout(done, 100);
4045
}
4146

47+
let profilerCount = 0;
48+
49+
const profiledComponents: {
50+
[key: string]: number;
51+
} = {};
52+
4253
/**
4354
* getInitActivity pushes activity based on React component mount
4455
* @param name displayName of component that started activity
4556
*/
4657
const getInitActivity = (name: string): number | null => {
4758
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
4859

49-
if (tracingIntegration !== null) {
50-
// tslint:disable-next-line:no-unsafe-any
51-
return (tracingIntegration as any).constructor.pushActivity(name, {
52-
description: `<${name}>`,
53-
op: 'react',
54-
});
60+
if (tracingIntegration === null) {
61+
logger.warn(
62+
`Unable to profile component ${name} due to invalid Tracing Integration. Please make sure to setup the Tracing integration.`,
63+
);
64+
65+
return null;
5566
}
5667

57-
logger.warn(
58-
`Unable to profile component ${name} due to invalid Tracing Integration. Please make sure to setup the Tracing integration.`,
59-
);
60-
return null;
68+
// tslint:disable-next-line:no-unsafe-any
69+
const activity = (tracingIntegration as any).constructor.pushActivity(name, {
70+
description: `<${name}>`,
71+
op: 'react',
72+
}) as number;
73+
74+
/**
75+
* If an activity was already generated, this the component is in React.StrictMode.
76+
* React.StrictMode will call constructors and setState hooks twice, effectively
77+
* creating redundant spans for every render (ex. two <App /> spans, two <Link /> spans)
78+
*
79+
* React.StrictMode only has this behaviour in Development Mode
80+
* See: https://reactjs.org/docs/strict-mode.html
81+
*
82+
* To account for this, we track all profiled components, and cancel activities that
83+
* we recognize to be coming from redundant push activity calls. It is important to note
84+
* that it is the first call to push activity that is invalid, as that is the one caused
85+
* by React.StrictMode.
86+
*
87+
*/
88+
if (isReactInDevMode()) {
89+
// We can make the guarantee here that if a redundant activity exists, it comes right
90+
// before the current activity, hence having a profilerCount one less than the existing count.
91+
const redundantActivity = profiledComponents[String(`${name}${profilerCount - 1}`)];
92+
93+
if (redundantActivity) {
94+
// tslint:disable-next-line:no-unsafe-any
95+
(tracingIntegration as any).constructor.cancelActivity(redundantActivity);
96+
} else {
97+
// If an redundant activity didn't exist, we can store the current activity to
98+
// check later. We have to do this inside an else block because of the case of
99+
// the edge case where two components may share a single components name.
100+
profiledComponents[String(`${name}${profilerCount}`)] = activity;
101+
}
102+
}
103+
104+
profilerCount += 1;
105+
return activity;
61106
};
62107

63108
export type ProfilerProps = {

0 commit comments

Comments
 (0)