1
1
import { getCurrentHub } from '@sentry/browser' ;
2
- import { Integration , IntegrationClass , Span } from '@sentry/types' ;
3
- import { logger } from '@sentry/utils' ;
2
+ import { Integration , IntegrationClass , Span , SpanContext } 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
@@ -49,20 +49,31 @@ const getTracingIntegration = () => {
49
49
return globalTracingIntegration ;
50
50
} ;
51
51
52
- /** JSDOC */
52
+ /**
53
+ * Warn if tracing integration not configured. Will only warn once.
54
+ */
53
55
function warnAboutTracing ( name : string ) : void {
54
56
if ( globalTracingIntegration === null ) {
55
57
logger . warn (
56
- `Unable to profile component ${ name } due to invalid Tracing Integration. Please make sure to setup the Tracing integration.` ,
58
+ `Unable to profile component ${ name } due to invalid Tracing Integration. Please make sure the Tracing integration is setup properly .` ,
57
59
) ;
58
60
}
59
61
}
60
62
61
63
/**
62
- * pushActivity creates an new react activity
64
+ * pushActivity creates an new react activity.
65
+ * Is a no-op if Tracing integration is not valid
63
66
* @param name displayName of component that started activity
64
67
*/
65
- const pushActivity = ( name : string , op : string , options ?: Object ) : number | null => {
68
+ function pushActivity (
69
+ name : string ,
70
+ op : string ,
71
+ context ?: SpanContext ,
72
+ options ?: {
73
+ autoPopAfter ?: number ;
74
+ parentSpanId ?: string ;
75
+ } ,
76
+ ) : number | null {
66
77
if ( globalTracingIntegration === null ) {
67
78
return null ;
68
79
}
@@ -73,47 +84,61 @@ const pushActivity = (name: string, op: string, options?: Object): number | null
73
84
{
74
85
description : `<${ name } >` ,
75
86
op : `react.${ op } ` ,
87
+ ...context ,
76
88
} ,
77
89
options ,
78
90
) ;
79
- } ;
91
+ }
80
92
81
93
/**
82
- * popActivity removes a React activity if it exists
94
+ * popActivity removes a React activity.
95
+ * Is a no-op if invalid Tracing integration or invalid activity id.
83
96
* @param activity id of activity that is being popped
97
+ * @param finish if a span should be finished after the activity is removed
84
98
*/
85
- const popActivity = ( activity : number | null ) : void => {
99
+ function popActivity ( activity : number | null , finish : boolean = true ) : void {
86
100
if ( activity === null || globalTracingIntegration === null ) {
87
101
return ;
88
102
}
89
103
90
104
// tslint:disable-next-line:no-unsafe-any
91
- ( globalTracingIntegration as any ) . constructor . popActivity ( activity ) ;
92
- } ;
105
+ ( globalTracingIntegration as any ) . constructor . popActivity ( activity , undefined , finish ) ;
106
+ }
107
+
108
+ function getActivitySpan ( activity : number | null ) : Span | undefined {
109
+ if ( globalTracingIntegration === null ) {
110
+ return undefined ;
111
+ }
112
+
113
+ // tslint:disable-next-line:no-unsafe-any
114
+ return ( globalTracingIntegration as any ) . constructor . getActivitySpan ( activity ) as Span | undefined ;
115
+ }
93
116
94
117
export type ProfilerProps = {
95
118
// The name of the component being profiled.
96
119
name : string ;
97
120
// If the Profiler is disabled. False by default.
98
121
disabled ?: boolean ;
122
+ // If component updates should be displayed as spans. False by default.
123
+ generateUpdateSpans ?: boolean ;
99
124
} ;
100
125
101
126
/**
102
127
* The Profiler component leverages Sentry's Tracing integration to generate
103
128
* spans based on component lifecycles.
104
129
*/
105
130
class Profiler extends React . Component < ProfilerProps > {
106
- public mountInfo : {
107
- // The activity representing when a component was mounted onto a page.
108
- activity : number | null ;
109
- // The span from the mountInfo activity
110
- span : Span | null ;
111
- } = {
112
- activity : null ,
113
- span : null ,
114
- } ;
131
+ // The activity representing how long it takes to mount a component.
132
+ public mountActivity : number | null = null ;
133
+ // The spanId of the mount activity
134
+ public mountSpanId : string | null = null ;
115
135
// The activity representing how long a component was on the page.
116
- public visibleActivity : number | null = null ;
136
+ public renderActivity : number | null = null ;
137
+
138
+ public static defaultProps : Partial < ProfilerProps > = {
139
+ disabled : false ,
140
+ generateUpdateSpans : false ,
141
+ } ;
117
142
118
143
public constructor ( props : ProfilerProps ) {
119
144
super ( props ) ;
@@ -124,7 +149,7 @@ class Profiler extends React.Component<ProfilerProps> {
124
149
}
125
150
126
151
if ( getTracingIntegration ( ) ) {
127
- this . mountInfo . activity = pushActivity ( name , 'mount' ) ;
152
+ this . mountActivity = pushActivity ( name , 'mount' ) ;
128
153
} else {
129
154
warnAboutTracing ( name ) ;
130
155
}
@@ -133,23 +158,44 @@ class Profiler extends React.Component<ProfilerProps> {
133
158
// If a component mounted, we can finish the mount activity.
134
159
public componentDidMount ( ) : void {
135
160
afterNextFrame ( ( ) => {
136
- popActivity ( this . mountInfo . activity ) ;
137
- this . mountInfo . activity = null ;
161
+ const span = getActivitySpan ( this . mountActivity ) ;
162
+ if ( span ) {
163
+ this . mountSpanId = span . spanId ;
164
+ }
165
+ popActivity ( this . mountActivity ) ;
166
+ this . mountActivity = null ;
138
167
139
- this . visibleActivity = pushActivity ( this . props . name , 'visible' ) ;
168
+ // If we were able to obtain the spanId of the mount activity, we should set the
169
+ // next activity as a child to the component mount activity.
170
+ const options = span ? { parentSpanId : span . spanId } : { } ;
171
+ this . renderActivity = pushActivity ( this . props . name , 'render' , { } , options ) ;
140
172
} ) ;
141
173
}
142
174
143
- // If a component doesn't mount, the visible activity will be end when the
175
+ public componentDidUpdate ( prevProps : ProfilerProps ) : void {
176
+ if ( prevProps . generateUpdateSpans && this . mountSpanId ) {
177
+ const now = timestampWithMs ( ) ;
178
+ const updateActivity = pushActivity (
179
+ prevProps . name ,
180
+ 'update' ,
181
+ {
182
+ endTimestamp : now ,
183
+ startTimestamp : now ,
184
+ } ,
185
+ { parentSpanId : this . mountSpanId } ,
186
+ ) ;
187
+ popActivity ( updateActivity , false ) ;
188
+ }
189
+ }
190
+
191
+ // If a component doesn't mount, the render activity will be end when the
144
192
public componentWillUnmount ( ) : void {
145
193
afterNextFrame ( ( ) => {
146
- popActivity ( this . visibleActivity ) ;
147
- this . visibleActivity = null ;
194
+ popActivity ( this . renderActivity , false ) ;
195
+ this . renderActivity = null ;
148
196
} ) ;
149
197
}
150
198
151
- public finishProfile = ( ) => { } ;
152
-
153
199
public render ( ) : React . ReactNode {
154
200
return this . props . children ;
155
201
}
@@ -191,15 +237,16 @@ function withProfiler<P extends object>(
191
237
* @param name displayName of component being profiled
192
238
*/
193
239
function useProfiler ( name : string ) : void {
194
- const [ activity ] = React . useState ( ( ) => pushActivity ( name ) ) ;
240
+ const [ activity ] = React . useState ( ( ) => pushActivity ( name , 'mount' ) ) ;
195
241
196
242
React . useEffect ( ( ) => {
197
243
afterNextFrame ( ( ) => {
198
- const tracingIntegration = getCurrentHub ( ) . getIntegration ( TRACING_GETTER ) ;
199
- if ( tracingIntegration !== null ) {
200
- // tslint:disable-next-line:no-unsafe-any
201
- ( tracingIntegration as any ) . constructor . popActivity ( activity ) ;
202
- }
244
+ popActivity ( activity ) ;
245
+ const renderActivity = pushActivity ( name , 'render' ) ;
246
+
247
+ return ( ) => {
248
+ popActivity ( renderActivity ) ;
249
+ } ;
203
250
} ) ;
204
251
} , [ ] ) ;
205
252
}
0 commit comments