Skip to content

Commit ebeaffe

Browse files
committed
ref(react): Split up HOC and Profiler
1 parent 5c9a6c1 commit ebeaffe

File tree

3 files changed

+99
-66
lines changed

3 files changed

+99
-66
lines changed

packages/react/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
import * as Sentry from '@sentry/browser';
1+
export * from '@sentry/browser';
22

3-
export { Sentry };
3+
import { Profiler, withProfiler } from './profiler';
4+
5+
export { Profiler, withProfiler };

packages/react/src/profiler.tsx

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@ import { Integration, IntegrationClass } from '@sentry/types';
33
import { logger } from '@sentry/utils';
44
import * as React from 'react';
55

6-
/** The Props Injected by the HOC */
7-
interface InjectedProps {
8-
/** Called when a transaction is finished */
9-
finishProfile(): void;
10-
}
11-
12-
const DEFAULT_DURATION = 30000;
6+
export const DEFAULT_DURATION = 30000;
7+
export const UNKNOWN_COMPONENT = 'unknown';
138

149
const TRACING_GETTER = ({
1510
id: 'Tracing',
1611
} as any) as IntegrationClass<Integration>;
1712

18-
const getInitActivity = (componentDisplayName: string, timeout = DEFAULT_DURATION): number | null => {
13+
const getInitActivity = (componentDisplayName: string, timeout: number): number | null => {
1914
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
2015

2116
if (tracingIntegration !== null) {
@@ -37,45 +32,63 @@ const getInitActivity = (componentDisplayName: string, timeout = DEFAULT_DURATIO
3732
return null;
3833
};
3934

40-
/**
41-
* withProfiler() is a HOC that leverages the Sentry AM tracing integration to
42-
* send transactions about a React component.
43-
* @param WrappedComponent The component profiled
44-
* @param timeout A maximum timeout for the component render
45-
*/
46-
export const withProfiler = <P extends object>(WrappedComponent: React.ComponentType<P>, timeout?: number) => {
47-
const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
35+
interface ProfilerProps {
36+
componentDisplayName?: string;
37+
timeout?: number;
38+
}
4839

49-
return class extends React.Component<Omit<P, keyof InjectedProps>, { activity: number | null }> {
50-
public static displayName: string = `profiler(${componentDisplayName})`;
40+
interface ProfilerState {
41+
activity: number | null;
42+
}
5143

52-
public constructor(props: P) {
53-
super(props);
44+
class Profiler extends React.Component<ProfilerProps, ProfilerState> {
45+
public constructor(props: ProfilerProps) {
46+
super(props);
5447

55-
this.state = {
56-
activity: getInitActivity(componentDisplayName, timeout),
57-
};
58-
}
48+
const { componentDisplayName = UNKNOWN_COMPONENT, timeout = DEFAULT_DURATION } = this.props;
5949

60-
public componentWillUnmount(): void {
61-
this.finishProfile();
62-
}
63-
64-
public finishProfile = () => {
65-
if (!this.state.activity) {
66-
return;
67-
}
68-
69-
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
70-
if (tracingIntegration !== null) {
71-
// tslint:disable-next-line:no-unsafe-any
72-
(tracingIntegration as any).constructor.popActivity(this.state.activity);
73-
this.setState({ activity: null });
74-
}
50+
this.state = {
51+
activity: getInitActivity(componentDisplayName, timeout),
7552
};
53+
}
54+
55+
public componentWillUnmount(): void {
56+
this.finishProfile();
57+
}
58+
59+
public finishProfile = () => {
60+
if (!this.state.activity) {
61+
return;
62+
}
7663

77-
public render(): React.ReactNode {
78-
return <WrappedComponent {...this.props as P} />;
64+
const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
65+
if (tracingIntegration !== null) {
66+
// tslint:disable-next-line:no-unsafe-any
67+
(tracingIntegration as any).constructor.popActivity(this.state.activity);
68+
this.setState({ activity: null });
7969
}
8070
};
81-
};
71+
72+
public render(): React.ReactNode {
73+
return this.props.children;
74+
}
75+
}
76+
77+
function withProfiler<P extends object>(
78+
WrappedComponent: React.ComponentType<P>,
79+
profilerProps?: ProfilerProps,
80+
): React.FC<P> {
81+
const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT;
82+
83+
const Wrapped: React.FC<P> = (props: P) => (
84+
<Profiler componentDisplayName={componentDisplayName} {...profilerProps}>
85+
<WrappedComponent {...props} />
86+
</Profiler>
87+
);
88+
89+
Wrapped.displayName = `profiler(${componentDisplayName})`;
90+
91+
return Wrapped;
92+
}
93+
94+
export { withProfiler, Profiler };

packages/react/test/profiler.test.tsx

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
2-
import { create, ReactTestInstance } from 'react-test-renderer';
2+
import { create } from 'react-test-renderer';
33

4-
import { withProfiler } from '../src/profiler';
4+
import { DEFAULT_DURATION, UNKNOWN_COMPONENT, withProfiler } from '../src/profiler';
55

66
const mockPushActivity = jest.fn().mockReturnValue(1);
77
const mockPopActivity = jest.fn();
@@ -38,14 +38,6 @@ describe('withProfiler', () => {
3838
mockPopActivity.mockClear();
3939
});
4040

41-
it('is called with pushActivity() when mounted', () => {
42-
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>);
43-
44-
expect(mockPushActivity).toHaveBeenCalledTimes(0);
45-
create(<ProfiledComponent />);
46-
expect(mockPushActivity).toHaveBeenCalledTimes(1);
47-
});
48-
4941
it('is called with popActivity() when unmounted', () => {
5042
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>);
5143

@@ -55,20 +47,46 @@ describe('withProfiler', () => {
5547
profiler.unmount();
5648

5749
expect(mockPopActivity).toHaveBeenCalledTimes(1);
50+
expect(mockPopActivity).toHaveBeenLastCalledWith(1);
5851
});
5952

60-
it('calls finishProfile() when unmounting', () => {
61-
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>);
62-
63-
const mockFinishProfile = jest.fn();
64-
const profiler = create(<ProfiledComponent />);
65-
66-
const instance = profiler.getInstance() as ReactTestInstance & { finishProfile(): void };
67-
instance.finishProfile = mockFinishProfile;
68-
69-
expect(mockFinishProfile).toHaveBeenCalledTimes(0);
70-
profiler.unmount();
71-
expect(mockFinishProfile).toHaveBeenCalledTimes(1);
53+
describe('pushActivity()', () => {
54+
it('is called when mounted', () => {
55+
const ProfiledComponent = withProfiler(() => <h1>Testing</h1>);
56+
57+
expect(mockPushActivity).toHaveBeenCalledTimes(0);
58+
create(<ProfiledComponent />);
59+
expect(mockPushActivity).toHaveBeenCalledTimes(1);
60+
expect(mockPushActivity).toHaveBeenLastCalledWith(
61+
UNKNOWN_COMPONENT,
62+
{
63+
data: {},
64+
description: `<${UNKNOWN_COMPONENT}>`,
65+
op: 'react',
66+
},
67+
{ autoPopAfter: DEFAULT_DURATION },
68+
);
69+
});
70+
71+
it('is called with a custom timeout', () => {
72+
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>, { timeout: 32 });
73+
74+
create(<ProfiledComponent />);
75+
expect(mockPushActivity).toHaveBeenLastCalledWith(expect.any(String), expect.any(Object), {
76+
autoPopAfter: 32,
77+
});
78+
});
79+
80+
it('is called with a custom displayName', () => {
81+
const ProfiledComponent = withProfiler(() => <h1>Hello World</h1>, { componentDisplayName: 'Test' });
82+
83+
create(<ProfiledComponent />);
84+
expect(mockPushActivity).toHaveBeenLastCalledWith(
85+
'Test',
86+
expect.objectContaining({ description: '<Test>' }),
87+
expect.any(Object),
88+
);
89+
});
7290
});
7391
});
7492
});

0 commit comments

Comments
 (0)