diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4187506b8ccf..bf29164124ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
- [react] feat: Add @sentry/react package (#2631)
- [react] feat: Add Error Boundary component (#2647)
+- [react] feat: Add useProfiler hook (#2659)
## 5.17.0
diff --git a/packages/react/package.json b/packages/react/package.json
index a0e8501e6cf5..215865a5f638 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -28,6 +28,7 @@
},
"devDependencies": {
"@testing-library/react": "^10.0.6",
+ "@testing-library/react-hooks": "^3.3.0",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/react": "^16.9.35",
"jest": "^24.7.1",
@@ -36,9 +37,11 @@
"prettier-check": "^2.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
+ "react-test-renderer": "^16.13.1",
"rimraf": "^2.6.3",
"tslint": "^5.16.0",
"tslint-react": "^5.0.0",
+ "tslint-react-hooks": "^2.2.2",
"typescript": "^3.5.1"
},
"scripts": {
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index aa30b551dddc..d6779b078372 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,4 +1,4 @@
export * from '@sentry/browser';
-export { Profiler, withProfiler } from './profiler';
+export { Profiler, withProfiler, useProfiler } from './profiler';
export { ErrorBoundary, withErrorBoundary } from './errorboundary';
diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx
index 00f404ce5a65..fec61437abbb 100644
--- a/packages/react/src/profiler.tsx
+++ b/packages/react/src/profiler.tsx
@@ -111,4 +111,25 @@ function withProfiler
(WrappedComponent: React.ComponentType
return Wrapped;
}
-export { withProfiler, Profiler };
+/**
+ *
+ * `useProfiler` is a React hook that profiles a React component.
+ *
+ * Requires React 16.8 or above.
+ * @param name displayName of component being profiled
+ */
+function useProfiler(name: string): void {
+ const activity = getInitActivity(name);
+
+ React.useEffect(() => {
+ afterNextFrame(() => {
+ const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER);
+ if (tracingIntegration !== null) {
+ // tslint:disable-next-line:no-unsafe-any
+ (tracingIntegration as any).constructor.popActivity(activity);
+ }
+ });
+ }, []);
+}
+
+export { withProfiler, Profiler, useProfiler };
diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx
index 008803e38ac4..da7861145bd4 100644
--- a/packages/react/test/profiler.test.tsx
+++ b/packages/react/test/profiler.test.tsx
@@ -1,7 +1,8 @@
import { render } from '@testing-library/react';
+import { renderHook } from '@testing-library/react-hooks';
import * as React from 'react';
-import { UNKNOWN_COMPONENT, withProfiler } from '../src/profiler';
+import { UNKNOWN_COMPONENT, useProfiler, withProfiler } from '../src/profiler';
const mockPushActivity = jest.fn().mockReturnValue(1);
const mockPopActivity = jest.fn();
@@ -25,6 +26,12 @@ jest.mock('@sentry/browser', () => ({
}));
describe('withProfiler', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ mockPushActivity.mockClear();
+ mockPopActivity.mockClear();
+ });
+
it('sets displayName properly', () => {
const TestComponent = () =>
Hello World
;
@@ -32,39 +39,60 @@ describe('withProfiler', () => {
expect(ProfiledComponent.displayName).toBe('profiler(TestComponent)');
});
- describe('Tracing Integration', () => {
- beforeEach(() => {
- jest.useFakeTimers();
- mockPushActivity.mockClear();
- mockPopActivity.mockClear();
- });
+ it('popActivity() is called when unmounted', () => {
+ const ProfiledComponent = withProfiler(() => Hello World
);
- it('is called with popActivity() when unmounted', () => {
- const ProfiledComponent = withProfiler(() => Hello World
);
+ expect(mockPopActivity).toHaveBeenCalledTimes(0);
+ const profiler = render();
+ profiler.unmount();
- expect(mockPopActivity).toHaveBeenCalledTimes(0);
+ jest.runAllTimers();
- const profiler = render();
- profiler.unmount();
+ expect(mockPopActivity).toHaveBeenCalledTimes(1);
+ expect(mockPopActivity).toHaveBeenLastCalledWith(1);
+ });
- jest.runAllTimers();
+ it('pushActivity() is called when mounted', () => {
+ const ProfiledComponent = withProfiler(() => Testing
);
- expect(mockPopActivity).toHaveBeenCalledTimes(1);
- expect(mockPopActivity).toHaveBeenLastCalledWith(1);
+ expect(mockPushActivity).toHaveBeenCalledTimes(0);
+ render();
+ expect(mockPushActivity).toHaveBeenCalledTimes(1);
+ expect(mockPushActivity).toHaveBeenLastCalledWith(UNKNOWN_COMPONENT, {
+ description: `<${UNKNOWN_COMPONENT}>`,
+ op: 'react',
});
+ });
+});
+
+describe('useProfiler()', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ mockPushActivity.mockClear();
+ mockPopActivity.mockClear();
+ });
+
+ it('popActivity() is called when unmounted', () => {
+ // tslint:disable-next-line: no-void-expression
+ const profiler = renderHook(() => useProfiler('Example'));
+ expect(mockPopActivity).toHaveBeenCalledTimes(0);
+ profiler.unmount();
+
+ jest.runAllTimers();
+
+ expect(mockPopActivity).toHaveBeenCalled();
+ expect(mockPopActivity).toHaveBeenLastCalledWith(1);
+ });
- describe('pushActivity()', () => {
- it('is called when mounted', () => {
- const ProfiledComponent = withProfiler(() => Testing
);
-
- expect(mockPushActivity).toHaveBeenCalledTimes(0);
- render();
- expect(mockPushActivity).toHaveBeenCalledTimes(1);
- expect(mockPushActivity).toHaveBeenLastCalledWith(UNKNOWN_COMPONENT, {
- description: `<${UNKNOWN_COMPONENT}>`,
- op: 'react',
- });
- });
+ it('pushActivity() is called when mounted', () => {
+ expect(mockPushActivity).toHaveBeenCalledTimes(0);
+ // tslint:disable-next-line: no-void-expression
+ const profiler = renderHook(() => useProfiler('Example'));
+ profiler.unmount();
+ expect(mockPushActivity).toHaveBeenCalledTimes(1);
+ expect(mockPushActivity).toHaveBeenLastCalledWith('Example', {
+ description: ``,
+ op: 'react',
});
});
});
diff --git a/packages/react/tslint.json b/packages/react/tslint.json
index d874694ddf24..16541fbae993 100644
--- a/packages/react/tslint.json
+++ b/packages/react/tslint.json
@@ -1,5 +1,5 @@
{
- "extends": ["@sentry/typescript/tslint", "tslint-react"],
+ "extends": ["@sentry/typescript/tslint", "tslint-react", "tslint-react-hooks"],
"rules": {
"no-implicit-dependencies": [
true,
diff --git a/yarn.lock b/yarn.lock
index 59040bf54b46..c71ead99e8bb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -119,7 +119,7 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
-"@babel/runtime@^7.10.2", "@babel/runtime@^7.7.4":
+"@babel/runtime@^7.10.2", "@babel/runtime@^7.5.4", "@babel/runtime@^7.7.4":
version "7.10.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
@@ -1195,6 +1195,14 @@
dom-accessibility-api "^0.4.4"
pretty-format "^25.5.0"
+"@testing-library/react-hooks@^3.3.0":
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.3.0.tgz#dc217bfce8e7c34a99c811d73d23feef957b7c1d"
+ integrity sha512-rE9geI1+HJ6jqXkzzJ6abREbeud6bLF8OmF+Vyc7gBoPwZAEVBYjbC1up5nNoVfYBhO5HUwdD4u9mTehAUeiyw==
+ dependencies:
+ "@babel/runtime" "^7.5.4"
+ "@types/testing-library__react-hooks" "^3.0.0"
+
"@testing-library/react@^10.0.6":
version "10.0.6"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.0.6.tgz#e1e569135badb7367cc6ac9823e376eccb0280e0"
@@ -1453,6 +1461,13 @@
dependencies:
"@types/react" "*"
+"@types/react-test-renderer@*":
+ version "16.9.2"
+ resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5"
+ integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw==
+ dependencies:
+ "@types/react" "*"
+
"@types/react@*", "@types/react@^16.9.35":
version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
@@ -1511,6 +1526,14 @@
dependencies:
pretty-format "^25.1.0"
+"@types/testing-library__react-hooks@^3.0.0":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897"
+ integrity sha512-dE8iMTuR5lzB+MqnxlzORlXzXyCL0EKfzH0w/lau20OpkHD37EaWjZDz0iNG8b71iEtxT4XKGmSKAGVEqk46mw==
+ dependencies:
+ "@types/react" "*"
+ "@types/react-test-renderer" "*"
+
"@types/testing-library__react@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-10.0.1.tgz#92bb4a02394bf44428e35f1da2970ed77f803593"
@@ -1787,11 +1810,32 @@ after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
-agent-base@4, agent-base@5, agent-base@6, agent-base@^4.3.0, agent-base@~4.2.0:
+agent-base@4, agent-base@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
+ integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
+agent-base@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==
+agent-base@6:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a"
+ integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==
+ dependencies:
+ debug "4"
+
+agent-base@~4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
agentkeepalive@^3.4.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67"
@@ -4509,6 +4553,18 @@ es-to-primitive@^1.1.1, es-to-primitive@^1.2.0:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
+es6-promise@^4.0.3:
+ version "4.2.8"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+ integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+ dependencies:
+ es6-promise "^4.0.3"
+
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -9303,7 +9359,7 @@ react-dom@^16.0.0:
prop-types "^15.6.2"
scheduler "^0.19.1"
-react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -9313,6 +9369,16 @@ react-is@^16.8.4:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
+react-test-renderer@^16.13.1:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
+ integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
+ dependencies:
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ react-is "^16.8.6"
+ scheduler "^0.19.1"
+
react@^16.0.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@@ -11034,6 +11100,11 @@ tslint-consistent-codestyle@^1.15.1:
tslib "^1.7.1"
tsutils "^2.29.0"
+tslint-react-hooks@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/tslint-react-hooks/-/tslint-react-hooks-2.2.2.tgz#4dc9b3986196802d45c11cc0bf6319a8116fe2ed"
+ integrity sha512-gtwA14+WevNUtlBhvAD5Ukpxt2qMegYI7IDD8zN/3JXLksdLdEuU/T/oqlI1CtZhMJffqyNn+aqq2oUqUFXiNA==
+
tslint-react@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-5.0.0.tgz#d0ae644e8163bdd3e134012e9353094904e8dd44"