11import { 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' ;
44import * as hoistNonReactStatic from 'hoist-non-react-statics' ;
55import * as React from 'react' ;
66
@@ -10,91 +10,167 @@ const TRACING_GETTER = ({
1010 id : 'Tracing' ,
1111} as any ) as IntegrationClass < Integration > ;
1212
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+
1323/**
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.
2725 */
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+ }
4032}
4133
4234/**
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
4437 * @param name displayName of component that started activity
4538 */
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 ;
5542 }
5643
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+ }
6278
6379export type ProfilerProps = {
80+ // The name of the component being profiled.
6481 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 } ;
6591} ;
6692
93+ /**
94+ * The Profiler component leverages Sentry's Tracing integration to generate
95+ * spans based on component lifecycles.
96+ */
6797class 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+
69111 public constructor ( props : ProfilerProps ) {
70112 super ( props ) ;
113+ const { name, disabled = false } = this . props ;
71114
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+ }
73124 }
74125
75126 // If a component mounted, we can finish the mount activity.
76127 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 ;
84131 }
85132
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+ }
89155 }
156+ }
90157
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+ } ) ;
96172 }
97- } ;
173+ }
98174
99175 public render ( ) : React . ReactNode {
100176 return this . props . children ;
@@ -103,16 +179,22 @@ class Profiler extends React.Component<ProfilerProps> {
103179
104180/**
105181 * 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.
107184 *
108185 * @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
110187 */
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 ;
113195
114196 const Wrapped : React . FC < P > = ( props : P ) => (
115- < Profiler name = { componentDisplayName } >
197+ < Profiler { ... options } name = { componentDisplayName } updateProps = { props } >
116198 < WrappedComponent { ...props } />
117199 </ Profiler >
118200 ) ;
@@ -132,17 +214,40 @@ function withProfiler<P extends object>(WrappedComponent: React.ComponentType<P>
132214 * Requires React 16.8 or above.
133215 * @param name displayName of component being profiled
134216 */
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+ } ) ;
137236
138237 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+ } ) ;
144249 }
145- } ) ;
250+ } ;
146251 } , [ ] ) ;
147252}
148253
0 commit comments