diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 00f404ce5a65..ad159a0e5ec2 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/browser'; -import { Integration, IntegrationClass } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { Integration, IntegrationClass, SpanContext, Transaction } from '@sentry/types'; +import { parseSemver, logger, SemVer, timestampWithMs } from '@sentry/utils'; import * as hoistNonReactStatic from 'hoist-non-react-statics'; import * as React from 'react'; @@ -39,6 +39,26 @@ function afterNextFrame(callback: Function): void { timeout = window.setTimeout(done, 100); } +/** + * isProfilingModeOn tells us if the React.Profiler will correctly + * Profile it's components. This is only active in development mode + * and in profiling mode. + * + * Learn how to do that here: https://gist.github.com/bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977 + */ +function isProfilingModeOn(): boolean { + const fake = React.createElement('div') as any; + + // tslint:disable-next-line: triple-equals no-unsafe-any + if (fake._owner != null && fake._owner.actualDuration != null) { + // if the component has a valid owner, and that owner has a duration + // React is profiling all it's components + return true; + } + + return false; +} + const getInitActivity = (name: string): number | null => { const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); @@ -46,7 +66,7 @@ const getInitActivity = (name: string): number | null => { // tslint:disable-next-line:no-unsafe-any return (tracingIntegration as any).constructor.pushActivity(name, { description: `<${name}>`, - op: 'react', + op: 'react.mount', }); } @@ -62,6 +82,9 @@ export type ProfilerProps = { class Profiler extends React.Component { public activity: number | null; + public hasProfilingMode: boolean | null = null; + public reactVersion: SemVer = parseSemver(React.version); + public constructor(props: ProfilerProps) { super(props); @@ -69,13 +92,65 @@ class Profiler extends React.Component { } public componentDidMount(): void { - afterNextFrame(this.finishProfile); + if (!this.hasProfilingMode) { + afterNextFrame(this.finishProfile); + } } public componentWillUnmount(): void { afterNextFrame(this.finishProfile); } + /** + * + * React calls handleProfilerRender() any time a component within the profiled + * tree “commits” an update. + * + */ + public handleProfilerRender = ( + // The id prop of the Profiler tree that has just committed + id: string, + // Identifies whether the tree has just been mounted for the first time + // or re-rendered due to a change in props, state, or hooks + phase: 'mount' | 'update', + // Time spent rendering the Profiler and its descendants for the current update + actualDuration: number, + // Duration of the most recent render time for each individual component within the Profiler tree + _baseDuration: number, + // Timestamp when React began rendering the current update + // pageload = startTime of 0 + _startTime: number, + // Timestamp when React committed the current update + _commitTime: number, + ) => { + if (phase === 'mount') { + afterNextFrame(this.finishProfile); + } + + const componentName = this.props.name === UNKNOWN_COMPONENT ? id : this.props.name; + + const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); + if (tracingIntegration !== null) { + // tslint:disable-next-line: no-unsafe-any + const activeTransaction = (tracingIntegration as any).constructor._activeTransaction as Transaction; + console.table({ id, phase, actualDuration, _baseDuration, _startTime, _commitTime }); + console.log(activeTransaction); + + if (activeTransaction) { + const now = timestampWithMs(); + const spanContext: SpanContext = { + description: `<${componentName}>`, + op: 'react.update', + startTimestamp: now - actualDuration, + }; + + const span = activeTransaction.startChild(spanContext); + + span.finish(now); + } + } + }; + public finishProfile = () => { if (!this.activity) { return; @@ -90,6 +165,29 @@ class Profiler extends React.Component { }; public render(): React.ReactNode { + const { name } = this.props; + + if ( + // React <= v16.4 + (this.reactVersion.major && this.reactVersion.major <= 15) || + (this.reactVersion.major === 16 && this.reactVersion.minor && this.reactVersion.minor <= 4) + ) { + return this.props.children; + } + + if (this.hasProfilingMode === null) { + // TODO: This should be a global check + this.hasProfilingMode = isProfilingModeOn(); + } + + if (this.hasProfilingMode) { + return ( + + {this.props.children} + + ); + } + return this.props.children; } } diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index cc63caa8f165..02d8690f9a06 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -413,7 +413,7 @@ const SEMVER_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\ /** * Represents Semantic Versioning object */ -interface SemVer { +export interface SemVer { major?: number; minor?: number; patch?: number;