diff --git a/desktop/app/src/index.tsx b/desktop/app/src/index.tsx index 05c2781d6..48712ae23 100644 --- a/desktop/app/src/index.tsx +++ b/desktop/app/src/index.tsx @@ -202,3 +202,4 @@ export {checkIdbIsInstalled} from './utils/iOSContainerUtility'; export {default as SidebarExtensions} from './fb-stubs/LayoutInspectorSidebarExtensions'; export {IDEFileResolver, IDEType} from './fb-stubs/IDEFileResolver'; export {renderMockFlipperWithPlugin} from './test-utils/createMockFlipperWithPlugin'; +export {Tracked} from 'flipper-plugin'; // To be able to use it in legacy plugins diff --git a/desktop/app/src/ui/components/Button.tsx b/desktop/app/src/ui/components/Button.tsx index 667573be6..aba99e5a8 100644 --- a/desktop/app/src/ui/components/Button.tsx +++ b/desktop/app/src/ui/components/Button.tsx @@ -383,7 +383,7 @@ function ClassicButton(props: Props) { /** * A simple button, used in many parts of the application. */ -export function SandyButton({ +function SandyButton({ compact, disabled, icon, diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 8f09482e9..2453d10a1 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -39,6 +39,7 @@ test('Correct top level API exposed', () => { "styled", "theme", "usePlugin", + "useTrackedCallback", "useValue", "withTrackingScope", ] diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 6ae754b33..daad65b5a 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -57,6 +57,7 @@ export { TrackingScope, setGlobalInteractionReporter as _setGlobalInteractionReporter, withTrackingScope, + useTrackedCallback, } from './ui/Tracked'; export {sleep} from './utils/sleep'; diff --git a/desktop/flipper-plugin/src/ui/Tracked.tsx b/desktop/flipper-plugin/src/ui/Tracked.tsx index d2fd4918c..86daef4e8 100644 --- a/desktop/flipper-plugin/src/ui/Tracked.tsx +++ b/desktop/flipper-plugin/src/ui/Tracked.tsx @@ -7,7 +7,7 @@ * @format */ -import React from 'react'; +import React, {useMemo} from 'react'; import {Children, cloneElement, createContext, useContext} from 'react'; import reactElementToJSXString from 'react-element-to-jsx-string'; @@ -97,28 +97,49 @@ export function Tracked({ }) as any; } +export function useTrackedCallback( + action: string, + fn: T, + deps: any[], +): T { + const scope = useContext(TrackingScopeContext); + return useMemo(() => { + return wrapInteractionHandler(fn, null, '', scope, action); + // eslint-disable-next-line + }, deps) as any; +} + // Exported for test -export function wrapInteractionHandler( - fn: Function, - element: React.ReactElement, +export function wrapInteractionHandler( + fn: T, + element: React.ReactElement | null | string, event: string, scope: string, action?: string, -) { +): T { function report(start: number, initialEnd: number, error?: any) { globalInteractionReporter({ duration: initialEnd - start, totalDuration: Date.now() - start, success: error ? 0 : 1, error: error ? '' + error : undefined, - componentType: describeElementType(element), - action: action ?? describeElement(element), + componentType: + element === null + ? 'unknown' + : typeof element === 'string' + ? element + : describeElementType(element), + action: + action ?? + (element && typeof element != 'string' + ? describeElement(element) + : 'unknown'), scope, event, }); } - return function trappedInteractionHandler(this: any) { + const res = function trappedInteractionHandler(this: any) { let res: any; const start = Date.now(); const r = report.bind(null, start); @@ -144,7 +165,9 @@ export function wrapInteractionHandler( r(initialEnd); } return res; - }; + } as any; + res.flipperTracked = true; // Avoid double wrapping / handling, if e.g. Button is wrapped in Tracked + return res; } export function describeElement(element: React.ReactElement): string { @@ -155,7 +178,7 @@ export function describeElement(element: React.ReactElement): string { if (typeof element.key === 'string') { return element.key; } - return reactElementToJSXString(element).substr(0, 200).replace(/\n/g, ' '); + return stringifyElement(element); } function describeElementType(element: React.ReactElement): string { @@ -171,7 +194,7 @@ export function withTrackingScope(Component: any) { return function WithTrackingScope(props: any) { const scope = Component.displayName ?? Component.name ?? Component.constructor?.name; - if (!scope) { + if (!scope || typeof scope !== 'string') { throw new Error('Failed to find component name for trackingScope'); } return ( @@ -181,3 +204,42 @@ export function withTrackingScope(Component: any) { ); }; } + +// @ts-ignore +global.FlipperTrackingScopeContext = TrackingScopeContext; +//@ts-ignore +global.FlipperTracked = Tracked; + +// @ts-ignore +global.flipperTrackInteraction = function flipperTrackInteraction( + elementType: string, + event: string, + scope: string, + action: string | React.ReactElement | null, + fn: Function, + ...args: any[] +): void { + // @ts-ignore + if (fn.flipperTracked) { + return fn(...args); + } + return wrapInteractionHandler( + fn, + elementType, + event, + scope, + !action + ? 'unknown action' + : typeof action === 'string' + ? action + : stringifyElement(action), + )(...args); +}; + +function stringifyElement(element: any): string { + if (!element) return 'unknown element'; + if (typeof element === 'string') return element; + if (Array.isArray(element)) + return element.filter(Boolean).map(stringifyElement).join(''); + return reactElementToJSXString(element).substr(0, 200).replace(/\n/g, ' '); +} diff --git a/desktop/patches/antd+4.6.6.patch b/desktop/patches/antd+4.6.6.patch new file mode 100644 index 000000000..2a4f99f80 --- /dev/null +++ b/desktop/patches/antd+4.6.6.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/antd/es/button/button.js b/node_modules/antd/es/button/button.js +index 8496110..35cce19 100644 +--- a/node_modules/antd/es/button/button.js ++++ b/node_modules/antd/es/button/button.js +@@ -186,6 +186,8 @@ var InternalButton = function InternalButton(props, ref) { + fixTwoCNChar(); + }, [buttonRef]); + ++ var scope = React.useContext(global.FlipperTrackingScopeContext); ++ + var handleClick = function handleClick(e) { + var onClick = props.onClick; + +@@ -194,7 +196,11 @@ var InternalButton = function InternalButton(props, ref) { + } + + if (onClick) { +- onClick(e); ++ global.flipperTrackInteraction( ++ 'Button', 'onClick', scope, props.title || props.children || props.icon, ++ onClick, ++ e ++ ); + } + }; + diff --git a/desktop/patches/rc-collapse+2.0.0.patch b/desktop/patches/rc-collapse+2.0.0.patch new file mode 100644 index 000000000..5acba1e3a --- /dev/null +++ b/desktop/patches/rc-collapse+2.0.0.patch @@ -0,0 +1,21 @@ +diff --git a/node_modules/rc-collapse/es/Panel.js b/node_modules/rc-collapse/es/Panel.js +index 13dc708..c5145e7 100644 +--- a/node_modules/rc-collapse/es/Panel.js ++++ b/node_modules/rc-collapse/es/Panel.js +@@ -82,6 +82,7 @@ var CollapsePanel = function (_Component) { + return React.createElement( + 'div', + { className: itemCls, style: style, id: id }, ++ React.createElement(global.FlipperTracked, { action: 'collapse:' + header }, + React.createElement( + 'div', + { +@@ -99,7 +100,7 @@ var CollapsePanel = function (_Component) { + { className: prefixCls + '-extra' }, + extra + ) +- ), ++ )), + React.createElement( + Animate, + { diff --git a/desktop/patches/rc-tabs+11.6.1.patch b/desktop/patches/rc-tabs+11.6.1.patch new file mode 100644 index 000000000..26450a6e9 --- /dev/null +++ b/desktop/patches/rc-tabs+11.6.1.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/rc-tabs/es/TabNavList/TabNode.js b/node_modules/rc-tabs/es/TabNavList/TabNode.js +index 2a69e83..e00ada1 100644 +--- a/node_modules/rc-tabs/es/TabNavList/TabNode.js ++++ b/node_modules/rc-tabs/es/TabNavList/TabNode.js +@@ -37,10 +37,19 @@ function TabNode(_ref, ref) { + } + + var removable = editable && closable !== false && !disabled; ++ var scope = React.useContext(global.FlipperTrackingScopeContext); + + function onInternalClick(e) { + if (disabled) return; +- onClick(e); ++ ++ global.flipperTrackInteraction( ++ 'Tabs', ++ 'onTabClick', ++ scope, ++ 'tab:' + key + ':' + tab, ++ onClick, ++ e ++ ); + } + + function onRemoveTab(event) {