Track plugin selection steps

Summary: Sets up some logging for the various drop-out points when going through the deeplink flow.

Reviewed By: lblasa

Differential Revision: D31345623

fbshipit-source-id: a06ca97c1e687e39ea97a1f47fd8bb614149056f
This commit is contained in:
Pascal Hartig
2021-10-05 11:37:28 -07:00
committed by Facebook GitHub Bot
parent bce2cdc316
commit 37529af074
6 changed files with 238 additions and 58 deletions

View File

@@ -99,11 +99,16 @@ test('Will throw error on invalid deeplinks', async () => {
).rejects.toThrowErrorMatchingInlineSnapshot(`"Unknown deeplink"`);
expect(logger.track).toHaveBeenCalledTimes(2);
expect(logger.track).toHaveBeenLastCalledWith('usage', 'deeplink', {
expect(logger.track).toHaveBeenLastCalledWith(
'usage',
'deeplink',
{
query: 'flipper://test',
state: 'ERROR',
errorMessage: 'Unknown deeplink',
});
},
undefined,
);
});
test('Will throw error on invalid protocol', async () => {
@@ -116,11 +121,16 @@ test('Will throw error on invalid protocol', async () => {
).rejects.toThrowErrorMatchingInlineSnapshot(`"Unknown deeplink"`);
expect(logger.track).toHaveBeenCalledTimes(2);
expect(logger.track).toHaveBeenLastCalledWith('usage', 'deeplink', {
expect(logger.track).toHaveBeenLastCalledWith(
'usage',
'deeplink',
{
query: 'notflipper://test',
state: 'ERROR',
errorMessage: 'Unknown deeplink',
});
},
undefined,
);
});
test('Will track deeplinks', async () => {
@@ -142,9 +152,14 @@ test('Will track deeplinks', async () => {
'flipper://open-plugin?plugin-id=TestPlugin&client=TestApp&payload=universe',
);
expect(logger.track).toHaveBeenCalledWith('usage', 'deeplink', {
expect(logger.track).toHaveBeenCalledWith(
'usage',
'deeplink',
{
query:
'flipper://open-plugin?plugin-id=TestPlugin&client=TestApp&payload=universe',
state: 'INIT',
});
},
undefined,
);
});

View File

@@ -20,22 +20,7 @@ import {selectPlugin, getAllClients} from './reducers/connections';
import {Dialog} from 'flipper-plugin';
import {handleOpenPluginDeeplink} from './dispatcher/handleOpenPluginDeeplink';
import {message} from 'antd';
type DeeplinkInteraction = {
state: 'INIT' | 'ERROR';
errorMessage?: string;
};
function track(
logger: Logger,
query: string,
interaction: DeeplinkInteraction,
) {
logger.track('usage', 'deeplink', {
...interaction,
query,
});
}
import {track} from './deeplinkTracking';
const UNKNOWN = 'Unknown deeplink';
/**
@@ -63,7 +48,7 @@ export async function handleDeeplink(
throw unknownError();
}
if (uri.href.startsWith('flipper://open-plugin')) {
return handleOpenPluginDeeplink(store, query);
return handleOpenPluginDeeplink(store, query, trackInteraction);
}
if (uri.pathname.match(/^\/*import\/*$/)) {
const url = uri.searchParams.get('url');

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Logger} from './fb-interfaces/Logger';
export type OpenPluginParams = {
pluginId: string;
client: string | undefined;
devices: string[];
payload: string | undefined;
};
export type DeeplinkInteraction = {
state:
| 'INIT'
| 'ERROR'
| 'PLUGIN_LIGHTHOUSE_BAIL'
| 'PLUGIN_STATUS_BAIL'
| 'PLUGIN_DEVICE_BAIL'
| 'PLUGIN_DEVICE_UNSUPPORTED'
| 'PLUGIN_CLIENT_UNSUPPORTED'
| 'PLUGIN_OPEN_SUCCESS';
errorMessage?: string;
plugin?: OpenPluginParams;
extra?: object;
};
export function track(
logger: Logger,
query: string,
interaction: DeeplinkInteraction,
) {
logger.track(
'usage',
'deeplink',
{
...interaction,
query,
},
interaction.plugin?.pluginId,
);
}

View File

@@ -19,11 +19,25 @@ import {
createState,
useValue,
DevicePluginClient,
Dialog,
} from 'flipper-plugin';
import {parseOpenPluginParams} from '../handleOpenPluginDeeplink';
import {handleDeeplink} from '../../deeplink';
import {selectPlugin} from '../../reducers/connections';
let origAlertImpl: any;
let origConfirmImpl: any;
beforeEach(() => {
origAlertImpl = Dialog.alert;
origConfirmImpl = Dialog.confirm;
});
afterEach(() => {
Dialog.alert = origAlertImpl;
Dialog.confirm = origConfirmImpl;
});
test('open-plugin deeplink parsing', () => {
const testpayload = 'http://www.google/?test=c o%20o+l';
const testLink =
@@ -83,6 +97,7 @@ test('Triggering a deeplink will work', async () => {
const {renderer, client, store, logger} = await renderMockFlipperWithPlugin(
definition,
);
logger.track = jest.fn();
expect(linksSeen).toEqual([]);
@@ -120,6 +135,33 @@ test('Triggering a deeplink will work', async () => {
</div>
</body>
`);
expect(logger.track).toHaveBeenCalledTimes(2);
expect(logger.track).toHaveBeenCalledWith(
'usage',
'deeplink',
{
query:
'flipper://open-plugin?plugin-id=TestPlugin&client=TestApp&payload=universe',
state: 'INIT',
},
undefined,
);
expect(logger.track).toHaveBeenCalledWith(
'usage',
'deeplink',
{
query:
'flipper://open-plugin?plugin-id=TestPlugin&client=TestApp&payload=universe',
state: 'PLUGIN_OPEN_SUCCESS',
plugin: {
client: 'TestApp',
devices: [],
payload: 'universe',
pluginId: 'TestPlugin',
},
},
'TestPlugin',
);
});
test('triggering a deeplink without applicable device can wait for a device', async () => {
@@ -283,3 +325,63 @@ test('triggering a deeplink without applicable client can wait for a device', as
</body>
`);
});
test('triggering a deeplink with incompatible device will cause bail', async () => {
const definition = TestUtils.createTestDevicePlugin(
{
Component() {
return <p>Hello</p>;
},
devicePlugin() {
return {};
},
},
{
id: 'DevicePlugin',
supportedDevices: [{os: 'iOS'}],
},
);
const {store, logger, createDevice} = await renderMockFlipperWithPlugin(
definition,
);
logger.track = jest.fn();
// Skipping user interactions.
Dialog.alert = (async () => {}) as any;
Dialog.confirm = (async () => {}) as any;
store.dispatch(
selectPlugin({selectedPlugin: 'nonexisting', deepLinkPayload: null}),
);
const handlePromise = handleDeeplink(
store,
logger,
`flipper://open-plugin?plugin-id=${definition.id}&devices=iOS`,
);
jest.runAllTimers();
// create a new device that doesn't match spec
createDevice({serial: 'device2', os: 'Android'});
// wait for dialogues
await handlePromise;
expect(logger.track).toHaveBeenCalledTimes(2);
expect(logger.track).toHaveBeenCalledWith(
'usage',
'deeplink',
{
plugin: {
client: undefined,
devices: ['iOS'],
payload: undefined,
pluginId: 'DevicePlugin',
},
query: 'flipper://open-plugin?plugin-id=DevicePlugin&devices=iOS',
state: 'PLUGIN_DEVICE_BAIL',
},
'DevicePlugin',
);
});

View File

@@ -18,7 +18,7 @@ import {UserNotSignedInError} from '../utils/errors';
import {selectPlugin, setPluginEnabled} from '../reducers/connections';
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
import {Typography} from 'antd';
import {getPluginStatus} from '../utils/pluginUtils';
import {getPluginStatus, PluginStatus} from '../utils/pluginUtils';
import {loadPluginsFromMarketplace} from './fb-stubs/pluginMarketplace';
import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
import {startPluginDownload} from '../reducers/pluginDownloads';
@@ -29,13 +29,7 @@ import Client from '../Client';
import {RocketOutlined} from '@ant-design/icons';
import {showEmulatorLauncher} from '../sandy-chrome/appinspect/LaunchEmulator';
import {getAllClients} from '../reducers/connections';
type OpenPluginParams = {
pluginId: string;
client: string | undefined;
devices: string[];
payload: string | undefined;
};
import {DeeplinkInteraction, OpenPluginParams} from '../deeplinkTracking';
export function parseOpenPluginParams(query: string): OpenPluginParams {
// 'flipper://open-plugin?plugin-id=graphql&client=facebook&devices=android,ios&chrome=1&payload='
@@ -54,15 +48,33 @@ export function parseOpenPluginParams(query: string): OpenPluginParams {
};
}
export async function handleOpenPluginDeeplink(store: Store, query: string) {
export async function handleOpenPluginDeeplink(
store: Store,
query: string,
trackInteraction: (interaction: DeeplinkInteraction) => void,
) {
const params = parseOpenPluginParams(query);
const title = `Opening plugin ${params.pluginId}`;
if (!(await verifyLighthouseAndUserLoggedIn(store, title))) {
trackInteraction({
state: 'PLUGIN_LIGHTHOUSE_BAIL',
plugin: params,
});
return;
}
await verifyFlipperIsUpToDate(title);
if (!(await verifyPluginStatus(store, params.pluginId, title))) {
const [pluginStatusResult, pluginStatus] = await verifyPluginStatus(
store,
params.pluginId,
title,
);
if (!pluginStatusResult) {
trackInteraction({
state: 'PLUGIN_STATUS_BAIL',
plugin: params,
extra: {pluginStatus},
});
return;
}
@@ -79,6 +91,10 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) {
isDevicePlugin,
);
if (deviceOrClient === false) {
trackInteraction({
state: 'PLUGIN_DEVICE_BAIL',
plugin: params,
});
return;
}
const client: Client | undefined = isDevicePlugin
@@ -95,6 +111,11 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) {
type: 'error',
message: `This plugin is not supported by device ${device.displayTitle()}`,
});
trackInteraction({
state: 'PLUGIN_DEVICE_UNSUPPORTED',
plugin: params,
extra: {device: device.displayTitle()},
});
return;
}
if (!isDevicePlugin && !client!.plugins.has(params.pluginId)) {
@@ -103,6 +124,12 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) {
type: 'error',
message: `This plugin is not supported by client ${client!.query.app}`,
});
trackInteraction({
state: 'PLUGIN_CLIENT_UNSUPPORTED',
plugin: params,
extra: {client: client!.query.app},
});
return;
}
// verify plugin enabled
@@ -137,6 +164,10 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) {
}),
);
}
trackInteraction({
state: 'PLUGIN_OPEN_SUCCESS',
plugin: params,
});
}
// check if user is connected to VPN and logged in. Returns true if OK, or false if aborted
@@ -269,7 +300,7 @@ async function verifyPluginStatus(
store: Store,
pluginId: string,
title: string,
): Promise<boolean> {
): Promise<[boolean, PluginStatus]> {
// make sure we have marketplace plugin data present
if (!isTest() && !store.getState().plugins.marketplacePlugins.length) {
// plugins not yet fetched
@@ -281,21 +312,21 @@ async function verifyPluginStatus(
const [status, reason] = getPluginStatus(store, pluginId);
switch (status) {
case 'ready':
return true;
return [true, status];
case 'unknown':
await Dialog.alert({
type: 'warning',
title,
message: `No plugin with id '${pluginId}' is known to Flipper. Please correct the deeplink, or install the plugin from NPM using the plugin manager.`,
});
return false;
return [false, status];
case 'failed':
await Dialog.alert({
type: 'error',
title,
message: `We found plugin '${pluginId}', but failed to load it: ${reason}. Please check the logs for more details`,
});
return false;
return [false, status];
case 'gatekeeped':
if (
!(await Dialog.confirm({
@@ -318,7 +349,7 @@ async function verifyPluginStatus(
},
}))
) {
return false;
return [false, status];
}
break;
case 'bundle_installable': {
@@ -328,7 +359,7 @@ async function verifyPluginStatus(
}
case 'marketplace_installable': {
if (!(await installMarketPlacePlugin(store, pluginId, title))) {
return false;
return [false, status];
}
break;
}

View File

@@ -402,19 +402,18 @@ export function computeActivePluginList({
return pluginList;
}
export function getPluginStatus(
store: Store,
id: string,
): [
state:
export type PluginStatus =
| 'ready'
| 'unknown'
| 'failed'
| 'gatekeeped'
| 'bundle_installable'
| 'marketplace_installable',
reason?: string,
] {
| 'marketplace_installable';
export function getPluginStatus(
store: Store,
id: string,
): [state: PluginStatus, reason?: string] {
const state: PluginsState = store.getState().plugins;
if (state.devicePlugins.has(id) || state.clientPlugins.has(id)) {
return ['ready'];