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,91 +10,167 @@ const TRACING_GETTER = ({
10
10
id : 'Tracing' ,
11
11
} as any ) as IntegrationClass < Integration > ;
12
12
13
+ let globalTracingIntegration : Integration | null = null ;
14
+ const getTracingIntegration = ( ) => {
15
+ if ( globalTracingIntegration ) {
16
+ return globalTracingIntegration ;
17
+ }
18
+
19
+ globalTracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
20
+ return globalTracingIntegration ;
21
+ } ;
22
+
13
23
/**
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
- *
24
+ * Warn if tracing integration not configured. Will only warn once.
27
25
*/
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 ) ;
26
+ function warnAboutTracing ( name : string ) : void {
27
+ if ( globalTracingIntegration === null ) {
28
+ logger . warn (
29
+ `Unable to profile component ${ name } due to invalid Tracing Integration. Please make sure the Tracing integration is setup properly.` ,
30
+ ) ;
31
+ }
40
32
}
41
33
42
34
/**
43
- * getInitActivity pushes activity based on React component mount
35
+ * pushActivity creates an new react activity.
36
+ * Is a no-op if Tracing integration is not valid
44
37
* @param name displayName of component that started activity
45
38
*/
46
- const getInitActivity = ( name : string ) : number | null => {
47
- const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
48
-
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
- } ) ;
39
+ function pushActivity ( name : string , op : string ) : number | null {
40
+ if ( globalTracingIntegration === null ) {
41
+ return null ;
55
42
}
56
43
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 ;
61
- } ;
44
+ // tslint:disable-next-line:no-unsafe-any
45
+ return ( globalTracingIntegration as any ) . constructor . pushActivity ( name , {
46
+ description : `<${ name } >` ,
47
+ op : `react.${ op } ` ,
48
+ } ) ;
49
+ }
50
+
51
+ /**
52
+ * popActivity removes a React activity.
53
+ * Is a no-op if Tracing integration is not valid.
54
+ * @param activity id of activity that is being popped
55
+ */
56
+ function popActivity ( activity : number | null ) : void {
57
+ if ( activity === null || globalTracingIntegration === null ) {
58
+ return ;
59
+ }
60
+
61
+ // tslint:disable-next-line:no-unsafe-any
62
+ ( globalTracingIntegration as any ) . constructor . popActivity ( activity ) ;
63
+ }
64
+
65
+ /**
66
+ * Obtain a span given an activity id.
67
+ * Is a no-op if Tracing integration is not valid.
68
+ * @param activity activity id associated with obtained span
69
+ */
70
+ function getActivitySpan ( activity : number | null ) : Span | undefined {
71
+ if ( activity === null || globalTracingIntegration === null ) {
72
+ return undefined ;
73
+ }
74
+
75
+ // tslint:disable-next-line:no-unsafe-any
76
+ return ( globalTracingIntegration as any ) . constructor . getActivitySpan ( activity ) as Span | undefined ;
77
+ }
62
78
63
79
export type ProfilerProps = {
80
+ // The name of the component being profiled.
64
81
name : string ;
82
+ // If the Profiler is disabled. False by default. This is useful if you want to disable profilers
83
+ // in certain environments.
84
+ disabled ?: boolean ;
85
+ // If time component is on page should be displayed as spans. True by default.
86
+ hasRenderSpan ?: boolean ;
87
+ // If component updates should be displayed as spans. True by default.
88
+ hasUpdateSpan ?: boolean ;
89
+ // props given to component being profiled.
90
+ updateProps : { [ key : string ] : any } ;
65
91
} ;
66
92
93
+ /**
94
+ * The Profiler component leverages Sentry's Tracing integration to generate
95
+ * spans based on component lifecycles.
96
+ */
67
97
class Profiler extends React . Component < ProfilerProps > {
68
- public activity : number | null ;
98
+ // The activity representing how long it takes to mount a component.
99
+ public mountActivity : number | null = null ;
100
+ // The span of the mount activity
101
+ public mountSpan : Span | undefined = undefined ;
102
+ // The span of the render
103
+ public renderSpan : Span | undefined = undefined ;
104
+
105
+ public static defaultProps : Partial < ProfilerProps > = {
106
+ disabled : false ,
107
+ hasRenderSpan : true ,
108
+ hasUpdateSpan : true ,
109
+ } ;
110
+
69
111
public constructor ( props : ProfilerProps ) {
70
112
super ( props ) ;
113
+ const { name, disabled = false } = this . props ;
71
114
72
- this . activity = getInitActivity ( this . props . name ) ;
115
+ if ( disabled ) {
116
+ return ;
117
+ }
118
+
119
+ if ( getTracingIntegration ( ) ) {
120
+ this . mountActivity = pushActivity ( name , 'mount' ) ;
121
+ } else {
122
+ warnAboutTracing ( name ) ;
123
+ }
73
124
}
74
125
75
126
// If a component mounted, we can finish the mount activity.
76
127
public componentDidMount ( ) : void {
77
- afterNextFrame ( this . finishProfile ) ;
78
- }
79
-
80
- // Sometimes a component will unmount first, so we make
81
- // sure to also finish the mount activity here.
82
- public componentWillUnmount ( ) : void {
83
- afterNextFrame ( this . finishProfile ) ;
128
+ this . mountSpan = getActivitySpan ( this . mountActivity ) ;
129
+ popActivity ( this . mountActivity ) ;
130
+ this . mountActivity = null ;
84
131
}
85
132
86
- public finishProfile = ( ) => {
87
- if ( ! this . activity ) {
88
- return ;
133
+ public componentDidUpdate ( { updateProps, hasUpdateSpan = true } : ProfilerProps ) : void {
134
+ // Only generate an update span if hasUpdateSpan is true, if there is a valid mountSpan,
135
+ // and if the updateProps have changed. It is ok to not do a deep equality check here as it is expensive.
136
+ // We are just trying to give baseline clues for further investigation.
137
+ if ( hasUpdateSpan && this . mountSpan && updateProps !== this . props . updateProps ) {
138
+ // See what props haved changed between the previous props, and the current props. This is
139
+ // set as data on the span. We just store the prop keys as the values could be potenially very large.
140
+ const changedProps = Object . keys ( updateProps ) . filter ( k => updateProps [ k ] !== this . props . updateProps [ k ] ) ;
141
+ if ( changedProps . length > 0 ) {
142
+ // The update span is a point in time span with 0 duration, just signifying that the component
143
+ // has been updated.
144
+ const now = timestampWithMs ( ) ;
145
+ this . mountSpan . startChild ( {
146
+ data : {
147
+ changedProps,
148
+ } ,
149
+ description : `<${ this . props . name } >` ,
150
+ endTimestamp : now ,
151
+ op : `react.update` ,
152
+ startTimestamp : now ,
153
+ } ) ;
154
+ }
89
155
}
156
+ }
90
157
91
- const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
92
- if ( tracingIntegration !== null ) {
93
- // tslint:disable-next-line:no-unsafe-any
94
- ( tracingIntegration as any ) . constructor . popActivity ( this . activity ) ;
95
- this . activity = null ;
158
+ // If a component is unmounted, we can say it is no longer on the screen.
159
+ // This means we can finish the span representing the component render.
160
+ public componentWillUnmount ( ) : void {
161
+ const { name, hasRenderSpan = true } = this . props ;
162
+
163
+ if ( this . mountSpan && hasRenderSpan ) {
164
+ // If we were able to obtain the spanId of the mount activity, we should set the
165
+ // next activity as a child to the component mount activity.
166
+ this . mountSpan . startChild ( {
167
+ description : `<${ name } >` ,
168
+ endTimestamp : timestampWithMs ( ) ,
169
+ op : `react.render` ,
170
+ startTimestamp : this . mountSpan . endTimestamp ,
171
+ } ) ;
96
172
}
97
- } ;
173
+ }
98
174
99
175
public render ( ) : React . ReactNode {
100
176
return this . props . children ;
@@ -103,16 +179,22 @@ class Profiler extends React.Component<ProfilerProps> {
103
179
104
180
/**
105
181
* withProfiler is a higher order component that wraps a
106
- * component in a {@link Profiler} component.
182
+ * component in a {@link Profiler} component. It is recommended that
183
+ * the higher order component be used over the regular {@link Profiler} component.
107
184
*
108
185
* @param WrappedComponent component that is wrapped by Profiler
109
- * @param name displayName of component being profiled
186
+ * @param options the { @link ProfilerProps} you can pass into the Profiler
110
187
*/
111
- function withProfiler < P extends object > ( WrappedComponent : React . ComponentType < P > , name ?: string ) : React . FC < P > {
112
- const componentDisplayName = name || WrappedComponent . displayName || WrappedComponent . name || UNKNOWN_COMPONENT ;
188
+ function withProfiler < P extends object > (
189
+ WrappedComponent : React . ComponentType < P > ,
190
+ // We do not want to have `updateProps` given in options, it is instead filled through the HOC.
191
+ options ?: Pick < Partial < ProfilerProps > , Exclude < keyof ProfilerProps , 'updateProps' > > ,
192
+ ) : React . FC < P > {
193
+ const componentDisplayName =
194
+ ( options && options . name ) || WrappedComponent . displayName || WrappedComponent . name || UNKNOWN_COMPONENT ;
113
195
114
196
const Wrapped : React . FC < P > = ( props : P ) => (
115
- < Profiler name = { componentDisplayName } >
197
+ < Profiler { ... options } name = { componentDisplayName } updateProps = { props } >
116
198
< WrappedComponent { ...props } />
117
199
</ Profiler >
118
200
) ;
@@ -132,17 +214,40 @@ function withProfiler<P extends object>(WrappedComponent: React.ComponentType<P>
132
214
* Requires React 16.8 or above.
133
215
* @param name displayName of component being profiled
134
216
*/
135
- function useProfiler ( name : string ) : void {
136
- const [ activity ] = React . useState ( ( ) => getInitActivity ( name ) ) ;
217
+ function useProfiler (
218
+ name : string ,
219
+ options : { disabled ?: boolean ; hasRenderSpan ?: boolean } = {
220
+ disabled : false ,
221
+ hasRenderSpan : true ,
222
+ } ,
223
+ ) : void {
224
+ const [ mountActivity ] = React . useState ( ( ) => {
225
+ if ( options && options . disabled ) {
226
+ return null ;
227
+ }
228
+
229
+ if ( getTracingIntegration ( ) ) {
230
+ return pushActivity ( name , 'mount' ) ;
231
+ }
232
+
233
+ warnAboutTracing ( name ) ;
234
+ return null ;
235
+ } ) ;
137
236
138
237
React . useEffect ( ( ) => {
139
- afterNextFrame ( ( ) => {
140
- const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
141
- if ( tracingIntegration !== null ) {
142
- // tslint:disable-next-line:no-unsafe-any
143
- ( tracingIntegration as any ) . constructor . popActivity ( activity ) ;
238
+ const mountSpan = getActivitySpan ( mountActivity ) ;
239
+ popActivity ( mountActivity ) ;
240
+
241
+ return ( ) => {
242
+ if ( mountSpan && options . hasRenderSpan ) {
243
+ mountSpan . startChild ( {
244
+ description : `<${ name } >` ,
245
+ endTimestamp : timestampWithMs ( ) ,
246
+ op : `react.render` ,
247
+ startTimestamp : mountSpan . endTimestamp ,
248
+ } ) ;
144
249
}
145
- } ) ;
250
+ } ;
146
251
} , [ ] ) ;
147
252
}
148
253
0 commit comments