diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx index ba1854285..8a62425e3 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -8,6 +8,9 @@ */ jest.mock('../../defaultPlugins'); +try { + jest.mock('../../fb/Logger', () => require('../../fb-stubs/Logger')); +} catch {} import dispatcher, { getDynamicPlugins, @@ -158,6 +161,7 @@ test('requirePlugin loads plugin', () => { entry: path.join(__dirname, 'TestPlugin'), version: '1.0.0', }); + expect(plugin).not.toBeNull(); expect(plugin!.prototype).toBeInstanceOf(FlipperPlugin); expect(plugin!.id).toBe(TestPlugin.id); }); diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index 9f389ee73..1c0fb407b 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -33,6 +33,7 @@ import semver from 'semver'; import {PluginDetails} from 'flipper-plugin-lib'; import {addNotification} from '../reducers/notifications'; import styled from '@emotion/styled'; +import {tryCatchReportPluginFailures, reportUsage} from '../utils/metrics'; // eslint-disable-next-line import/no-unresolved import getPluginIndex from '../utils/getDefaultPluginsIndex'; @@ -58,6 +59,7 @@ export default (store: Store, logger: Logger) => { const initialPlugins: Array< typeof FlipperPlugin | typeof FlipperDevicePlugin > = filterNewestVersionOfEachPlugin(getBundledPlugins(), getDynamicPlugins()) + .map(reportVersion) .filter(checkDisabled(disabledPlugins)) .filter(checkGK(gatekeepedPlugins)) .map(requirePlugin(failedPlugins, defaultPluginsIndex)) @@ -70,7 +72,6 @@ export default (store: Store, logger: Logger) => { const deprecatedSpecPlugins = initialPlugins.filter( (p) => !p.isDefault && p.details.specVersion === 1, ); - for (const plugin of deprecatedSpecPlugins) { store.dispatch( addNotification({ @@ -149,6 +150,17 @@ export default (store: Store, logger: Logger) => { ); }; +function reportVersion(pluginDetails: PluginDetails) { + reportUsage( + 'plugin:version', + { + version: pluginDetails.version, + }, + pluginDetails.id, + ); + return pluginDetails; +} + export function filterNewestVersionOfEachPlugin( bundledPlugins: PluginDetails[], dynamicPlugins: PluginDetails[], @@ -237,33 +249,43 @@ export const requirePlugin = ( pluginDetails: PluginDetails, ): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => { try { - let plugin = pluginDetails.isDefault - ? defaultPluginsIndex[pluginDetails.name] - : reqFn(pluginDetails.entry); - if (plugin.default) { - plugin = plugin.default; - } - if (!(plugin.prototype instanceof FlipperBasePlugin)) { - throw new Error(`Plugin ${plugin.name} is not a FlipperBasePlugin`); - } - - plugin.id = plugin.id || pluginDetails.id; - plugin.packageName = pluginDetails.name; - plugin.details = pluginDetails; - - // set values from package.json as static variables on class - Object.keys(pluginDetails).forEach((key) => { - if (key !== 'name' && key !== 'id') { - plugin[key] = - plugin[key] || pluginDetails[key as keyof PluginDetails]; - } - }); - - return plugin; + return tryCatchReportPluginFailures( + () => requirePluginInternal(pluginDetails, defaultPluginsIndex, reqFn), + 'plugin:load', + pluginDetails.id, + ); } catch (e) { failedPlugins.push([pluginDetails, e.message]); - console.error(pluginDetails, e); + console.error(`Plugin ${pluginDetails.id} failed to load`, e); return null; } }; }; + +const requirePluginInternal = ( + pluginDetails: PluginDetails, + defaultPluginsIndex: any, + reqFn: Function = global.electronRequire, +) => { + let plugin = pluginDetails.isDefault + ? defaultPluginsIndex[pluginDetails.name] + : reqFn(pluginDetails.entry); + if (plugin.default) { + plugin = plugin.default; + } + if (!(plugin.prototype instanceof FlipperBasePlugin)) { + throw new Error(`Plugin ${plugin.name} is not a FlipperBasePlugin`); + } + + plugin.id = plugin.id || pluginDetails.id; + plugin.packageName = pluginDetails.name; + plugin.details = pluginDetails; + + // set values from package.json as static variables on class + Object.keys(pluginDetails).forEach((key) => { + if (key !== 'name' && key !== 'id') { + plugin[key] = plugin[key] || pluginDetails[key as keyof PluginDetails]; + } + }); + return plugin; +}; diff --git a/desktop/app/src/utils/metrics.tsx b/desktop/app/src/utils/metrics.tsx index 9aa90cd5e..d3004e07e 100644 --- a/desktop/app/src/utils/metrics.tsx +++ b/desktop/app/src/utils/metrics.tsx @@ -110,6 +110,29 @@ export function tryCatchReportPlatformFailures( } } +/* + * Wraps a closure, preserving it's functionality but logging the success or + failure state of it. + */ +export function tryCatchReportPluginFailures( + closure: () => T, + name: string, + plugin: string, +): T { + try { + const result = closure(); + logPluginSuccessRate(name, plugin, {kind: 'success'}); + return result; + } catch (e) { + logPluginSuccessRate(name, plugin, { + kind: 'failure', + supportedOperation: !(e instanceof UnsupportedError), + error: e, + }); + throw e; + } +} + /** * Track usage of a feature. * @param action Unique name for the action performed. E.g. captureScreenshot