Wire up Tracking to different ant.design elements

Summary:
This wires up tracking directly to the ANT component library for the following components:

1. `Button`
2. `Collapse.Panel`
3. `Tabs`

Other less commonly used elements can be connected in the future if needed.
I played a bit with different patterns, but in testing the patch-package patching give the most reliable results. Alternatives considered:

1. Expect users to explicitly wrap there components, e.g. `<Tracked><Button>Hi</Button></Tracked>`
    1. Didn't implement this because it would be very common to forget, and at the moment you want to make some analysis you'll discover there is no interesting data available. I think for tracking we want to have opt-out rather than opt-in
    2. The additional wrapping can cause some subtile layout issues due to static field inspection / forwarded refs (e.g. Ant often has an assumption that relevant children types are _directly_ nested under their parent element. For examle `<Tooltip><Tracked><Button>` does not work as expected
2. Expose our own `Button` / `Collapse` / `Tabs` that applies `Tracked` to an underlying Ant component.
    1. also suffers from 1.b.
    2. It is gonna be quite confusing for other devs that some elements would need to be imported from `flipper-plugin`, ant some from `antd`, and that this is likely to change over time. We could lint against it, but it will be still suboptimal

Reviewed By: jknoxville

Differential Revision: D25196321

fbshipit-source-id: b559356498c3191a283062a88daacb354b0f79f4
This commit is contained in:
Michel Weststrate
2020-12-03 04:13:07 -08:00
committed by Facebook GitHub Bot
parent 3394f85fc7
commit dd6f39c2b3
8 changed files with 149 additions and 12 deletions

View File

@@ -202,3 +202,4 @@ export {checkIdbIsInstalled} from './utils/iOSContainerUtility';
export {default as SidebarExtensions} from './fb-stubs/LayoutInspectorSidebarExtensions'; export {default as SidebarExtensions} from './fb-stubs/LayoutInspectorSidebarExtensions';
export {IDEFileResolver, IDEType} from './fb-stubs/IDEFileResolver'; export {IDEFileResolver, IDEType} from './fb-stubs/IDEFileResolver';
export {renderMockFlipperWithPlugin} from './test-utils/createMockFlipperWithPlugin'; export {renderMockFlipperWithPlugin} from './test-utils/createMockFlipperWithPlugin';
export {Tracked} from 'flipper-plugin'; // To be able to use it in legacy plugins

View File

@@ -383,7 +383,7 @@ function ClassicButton(props: Props) {
/** /**
* A simple button, used in many parts of the application. * A simple button, used in many parts of the application.
*/ */
export function SandyButton({ function SandyButton({
compact, compact,
disabled, disabled,
icon, icon,

View File

@@ -39,6 +39,7 @@ test('Correct top level API exposed', () => {
"styled", "styled",
"theme", "theme",
"usePlugin", "usePlugin",
"useTrackedCallback",
"useValue", "useValue",
"withTrackingScope", "withTrackingScope",
] ]

View File

@@ -57,6 +57,7 @@ export {
TrackingScope, TrackingScope,
setGlobalInteractionReporter as _setGlobalInteractionReporter, setGlobalInteractionReporter as _setGlobalInteractionReporter,
withTrackingScope, withTrackingScope,
useTrackedCallback,
} from './ui/Tracked'; } from './ui/Tracked';
export {sleep} from './utils/sleep'; export {sleep} from './utils/sleep';

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import React from 'react'; import React, {useMemo} from 'react';
import {Children, cloneElement, createContext, useContext} from 'react'; import {Children, cloneElement, createContext, useContext} from 'react';
import reactElementToJSXString from 'react-element-to-jsx-string'; import reactElementToJSXString from 'react-element-to-jsx-string';
@@ -97,28 +97,49 @@ export function Tracked({
}) as any; }) as any;
} }
export function useTrackedCallback<T extends Function>(
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 // Exported for test
export function wrapInteractionHandler( export function wrapInteractionHandler<T extends Function>(
fn: Function, fn: T,
element: React.ReactElement, element: React.ReactElement | null | string,
event: string, event: string,
scope: string, scope: string,
action?: string, action?: string,
) { ): T {
function report(start: number, initialEnd: number, error?: any) { function report(start: number, initialEnd: number, error?: any) {
globalInteractionReporter({ globalInteractionReporter({
duration: initialEnd - start, duration: initialEnd - start,
totalDuration: Date.now() - start, totalDuration: Date.now() - start,
success: error ? 0 : 1, success: error ? 0 : 1,
error: error ? '' + error : undefined, error: error ? '' + error : undefined,
componentType: describeElementType(element), componentType:
action: action ?? describeElement(element), element === null
? 'unknown'
: typeof element === 'string'
? element
: describeElementType(element),
action:
action ??
(element && typeof element != 'string'
? describeElement(element)
: 'unknown'),
scope, scope,
event, event,
}); });
} }
return function trappedInteractionHandler(this: any) { const res = function trappedInteractionHandler(this: any) {
let res: any; let res: any;
const start = Date.now(); const start = Date.now();
const r = report.bind(null, start); const r = report.bind(null, start);
@@ -144,7 +165,9 @@ export function wrapInteractionHandler(
r(initialEnd); r(initialEnd);
} }
return res; 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 { export function describeElement(element: React.ReactElement): string {
@@ -155,7 +178,7 @@ export function describeElement(element: React.ReactElement): string {
if (typeof element.key === 'string') { if (typeof element.key === 'string') {
return element.key; return element.key;
} }
return reactElementToJSXString(element).substr(0, 200).replace(/\n/g, ' '); return stringifyElement(element);
} }
function describeElementType(element: React.ReactElement): string { function describeElementType(element: React.ReactElement): string {
@@ -171,7 +194,7 @@ export function withTrackingScope(Component: any) {
return function WithTrackingScope(props: any) { return function WithTrackingScope(props: any) {
const scope = const scope =
Component.displayName ?? Component.name ?? Component.constructor?.name; Component.displayName ?? Component.name ?? Component.constructor?.name;
if (!scope) { if (!scope || typeof scope !== 'string') {
throw new Error('Failed to find component name for trackingScope'); throw new Error('Failed to find component name for trackingScope');
} }
return ( 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, ' ');
}

View File

@@ -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
+ );
}
};

View File

@@ -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,
{

View File

@@ -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) {