1
1
import { getCurrentHub } from '@sentry/browser' ;
2
- import { Integration , IntegrationClass } from '@sentry/types' ;
3
- import { logger } from '@sentry/utils' ;
2
+ import { Integration , IntegrationClass , Span } from '@sentry/types' ;
3
+ import { logger , timestampWithMs } from '@sentry/utils' ;
4
4
import * as hoistNonReactStatic from 'hoist-non-react-statics' ;
5
5
import * as React from 'react' ;
6
6
@@ -10,11 +10,6 @@ const TRACING_GETTER = ({
10
10
id : 'Tracing' ,
11
11
} as any ) as IntegrationClass < Integration > ;
12
12
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
-
18
13
/**
19
14
*
20
15
* Based on implementation from Preact:
@@ -44,103 +39,100 @@ function afterNextFrame(callback: Function): void {
44
39
timeout = window . setTimeout ( done , 100 ) ;
45
40
}
46
41
47
- let profilerCount = 0 ;
48
-
49
- const profiledComponents : {
50
- [ key : string ] : number ;
51
- } = { } ;
52
-
53
- /**
54
- * getInitActivity pushes activity based on React component mount
55
- * @param name displayName of component that started activity
56
- */
57
- const getInitActivity = ( name : string ) : number | null => {
58
- const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
59
-
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 ;
66
- }
67
-
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 [ `${ 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 [ `${ name } ${ profilerCount } ` ] = activity ;
101
- }
102
- }
42
+ function warnAboutTracing ( name : string ) : void {
43
+ logger . warn (
44
+ `Unable to profile component ${ name } due to invalid Tracing Integration. Please make sure to setup the Tracing integration.` ,
45
+ ) ;
46
+ }
103
47
104
- profilerCount += 1 ;
105
- return activity ;
106
- } ;
48
+ enum ReactOp {
49
+ Mount = 'mount' ,
50
+ Visible = 'visible' ,
51
+ }
107
52
108
53
export type ProfilerProps = {
109
54
name : string ;
110
55
} ;
111
56
112
57
class Profiler extends React . Component < ProfilerProps > {
113
- public activity : number | null ;
58
+ public tracingIntegration : Integration | null = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
59
+ public mountInfo : {
60
+ activity : number | null ;
61
+ span : Span | null ;
62
+ } = {
63
+ activity : null ,
64
+ span : null ,
65
+ } ;
66
+ public visibleActivity : number | null = null ;
114
67
115
68
public constructor ( props : ProfilerProps ) {
116
69
super ( props ) ;
117
70
118
- this . activity = getInitActivity ( this . props . name ) ;
71
+ if ( this . tracingIntegration === null ) {
72
+ warnAboutTracing ( props . name ) ;
73
+ } else {
74
+ // tslint:disable-next-line:no-unsafe-any
75
+ const activity = ( this . tracingIntegration as any ) . constructor . pushActivity ( props . name , {
76
+ data : {
77
+ update : 0 ,
78
+ } ,
79
+ description : `<${ props . name } >` ,
80
+ op : `react.${ ReactOp . Mount } ` ,
81
+ } ) as number ;
82
+
83
+ if ( activity ) {
84
+ this . mountInfo . activity = activity ;
85
+ // tslint:disable-next-line: no-unsafe-any
86
+ this . mountInfo . span = ( this . tracingIntegration as any ) . constructor . getActivitySpan ( activity ) ;
87
+ }
88
+ }
119
89
}
120
90
121
91
// If a component mounted, we can finish the mount activity.
122
92
public componentDidMount ( ) : void {
123
- afterNextFrame ( this . finishProfile ) ;
124
- }
93
+ afterNextFrame ( ( ) => {
94
+ if ( this . tracingIntegration === null ) {
95
+ return ;
96
+ }
125
97
126
- // Sometimes a component will unmount first, so we make
127
- // sure to also finish the mount activity here.
128
- public componentWillUnmount ( ) : void {
129
- afterNextFrame ( this . finishProfile ) ;
130
- }
98
+ if ( this . mountInfo . activity ) {
99
+ // tslint:disable-next-line:no-unsafe-any
100
+ ( this . tracingIntegration as any ) . constructor . popActivity ( this . mountInfo . activity ) ;
101
+ this . mountInfo . activity = null ;
102
+ }
131
103
132
- public finishProfile = ( ) => {
133
- if ( ! this . activity ) {
134
- return ;
135
- }
104
+ if ( this . mountInfo . span ) {
105
+ // tslint:disable-next-line:no-unsafe-any
106
+ this . visibleActivity = ( this . tracingIntegration as any ) . constructor . pushActivity (
107
+ this . props . name ,
108
+ {
109
+ description : `<${ this . props . name } >` ,
110
+ op : `react.${ ReactOp . Visible } ` ,
111
+ } ,
112
+ { parentSpanId : this . mountInfo . span . spanId , canBeCancelled : true } ,
113
+ ) as number ;
114
+ }
115
+ } ) ;
116
+ }
136
117
137
- const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
138
- if ( tracingIntegration !== null ) {
118
+ public componentDidUpdate ( ) : void {
119
+ if ( this . tracingIntegration !== null && this . mountInfo . span && this . mountInfo . span . data . update ) {
139
120
// tslint:disable-next-line:no-unsafe-any
140
- ( tracingIntegration as any ) . constructor . popActivity ( this . activity ) ;
141
- this . activity = null ;
121
+ this . mountInfo . span . setData ( 'update' , ( this . mountInfo . span . data . update += 1 ) ) ;
142
122
}
143
- } ;
123
+ }
124
+
125
+ // If a component doesn't mount, the visible activity will be end when the
126
+ // transaction ends.
127
+ public componentWillUnmount ( ) : void {
128
+ afterNextFrame ( ( ) => {
129
+ if ( this . visibleActivity && this . tracingIntegration !== null ) {
130
+ // tslint:disable-next-line:no-unsafe-any
131
+ ( this . tracingIntegration as any ) . constructor . popActivity ( this . visibleActivity ) ;
132
+ this . visibleActivity = null ;
133
+ }
134
+ } ) ;
135
+ }
144
136
145
137
public render ( ) : React . ReactNode {
146
138
return this . props . children ;
@@ -179,14 +171,59 @@ function withProfiler<P extends object>(WrappedComponent: React.ComponentType<P>
179
171
* @param name displayName of component being profiled
180
172
*/
181
173
function useProfiler ( name : string ) : void {
182
- const [ activity ] = React . useState ( ( ) => getInitActivity ( name ) ) ;
174
+ const [ mountActivity ] = React . useState ( ( ) => {
175
+ const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
176
+
177
+ if ( tracingIntegration !== null ) {
178
+ // tslint:disable-next-line: no-unsafe-any
179
+ return ( tracingIntegration as any ) . constructor . pushActivity ( name , {
180
+ description : `<${ name } >` ,
181
+ op : `react.${ ReactOp . Mount } ` ,
182
+ startTimestamp : timestampWithMs ( ) ,
183
+ } ) as number ;
184
+ }
185
+
186
+ warnAboutTracing ( name ) ;
187
+ return null ;
188
+ } ) ;
189
+
190
+ const [ visibleActivity ] = React . useState ( ( ) => {
191
+ const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
192
+
193
+ if ( tracingIntegration !== null ) {
194
+ // tslint:disable-next-line: no-unsafe-any
195
+ return ( tracingIntegration as any ) . constructor . pushActivity (
196
+ name ,
197
+ {
198
+ description : `<${ name } >` ,
199
+ op : `react.${ ReactOp . Visible } ` ,
200
+ startTimestamp : timestampWithMs ( ) ,
201
+ } ,
202
+ { autoPopAfter : 0 } ,
203
+ ) as number ;
204
+ }
205
+
206
+ warnAboutTracing ( name ) ;
207
+ return null ;
208
+ } ) ;
183
209
184
210
React . useEffect ( ( ) => {
185
211
afterNextFrame ( ( ) => {
186
212
const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
213
+
214
+ if ( tracingIntegration !== null ) {
215
+ // tslint:disable-next-line:no-unsafe-any
216
+ ( tracingIntegration as any ) . constructor . popActivity ( mountActivity ) ;
217
+ }
218
+ } ) ;
219
+
220
+ // tslint:disable-next-line: no-void-expression
221
+ return afterNextFrame ( ( ) => {
222
+ const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
223
+
187
224
if ( tracingIntegration !== null ) {
188
225
// tslint:disable-next-line:no-unsafe-any
189
- ( tracingIntegration as any ) . constructor . popActivity ( activity ) ;
226
+ ( tracingIntegration as any ) . constructor . popActivity ( visibleActivity ) ;
190
227
}
191
228
} ) ;
192
229
} , [ ] ) ;
0 commit comments