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"