From e1f6f770cd7d6cee5bbadc6db8e1236b43b53a52 Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Thu, 22 Apr 2021 16:46:03 -0700 Subject: [PATCH] Collect information about connected device and app as a part of plugin usage analytics Summary: We received questions several times from plugin authors on how to find usage for particular app/platform and I wanted to add that data on plugin usage dashboard, but right now it's impossible. We're only collecting plugin ID as a part of plugin usage analytics. That means it's impossible to filter some plugin usage (e.g. "Network") by particular platform or mobile app. This diff improves plugin usage analytics to collect enhanced data. Reviewed By: passy Differential Revision: D27935801 fbshipit-source-id: c2fc7d8cf84f9a28823ae56a1dda7158e0b12f1f --- .../dispatcher/__tests__/tracking.node.tsx | 114 +++++++++++++----- desktop/app/src/dispatcher/tracking.tsx | 42 +++++-- desktop/app/src/reducers/usageTracking.tsx | 42 ++++++- 3 files changed, 157 insertions(+), 41 deletions(-) diff --git a/desktop/app/src/dispatcher/__tests__/tracking.node.tsx b/desktop/app/src/dispatcher/__tests__/tracking.node.tsx index 1c485aa94..d99a2fe67 100644 --- a/desktop/app/src/dispatcher/__tests__/tracking.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/tracking.node.tsx @@ -7,15 +7,34 @@ * @format */ -import {computeUsageSummary, UsageSummary} from '../tracking'; -import {State} from '../../reducers/usageTracking'; +import {computeUsageSummary} from '../tracking'; +import {SelectedPluginData, State} from '../../reducers/usageTracking'; +import BaseDevice from '../../devices/BaseDevice'; +import {getPluginKey} from '../../utils/pluginUtils'; + +const device = new BaseDevice('serial', 'emulator', 'test device', 'iOS'); +const layoutPluginKey = getPluginKey('Facebook', device, 'Layout'); +const networkPluginKey = getPluginKey('Facebook', device, 'Network'); +const databasesPluginKey = getPluginKey('Facebook', device, 'Databases'); +const pluginData: SelectedPluginData = { + plugin: 'Layout', + app: 'Facebook', + device: 'test device', + deviceName: 'test device', + deviceSerial: 'serial', + deviceType: 'emulator', + os: 'iOS', + archived: false, +}; +const pluginData2 = {...pluginData, plugin: 'Network'}; +const pluginData3 = {...pluginData, plugin: 'Databases'}; test('Never focused', () => { const state: State = { timeline: [{type: 'TIMELINE_START', time: 100, isFocused: false}], }; const result = computeUsageSummary(state, 200); - expect(result).toReportTimeSpent('total', 0, 100); + expect(result.total).toReportTimeSpent('total', 0, 100); }); test('Always focused', () => { @@ -23,7 +42,7 @@ test('Always focused', () => { timeline: [{type: 'TIMELINE_START', time: 100, isFocused: true}], }; const result = computeUsageSummary(state, 200); - expect(result).toReportTimeSpent('total', 100, 0); + expect(result.total).toReportTimeSpent('total', 100, 0); }); test('Focused then unfocused', () => { @@ -34,7 +53,7 @@ test('Focused then unfocused', () => { ], }; const result = computeUsageSummary(state, 350); - expect(result).toReportTimeSpent('total', 50, 200); + expect(result.total).toReportTimeSpent('total', 50, 200); }); test('Unfocused then focused', () => { @@ -45,7 +64,7 @@ test('Unfocused then focused', () => { ], }; const result = computeUsageSummary(state, 350); - expect(result).toReportTimeSpent('total', 200, 50); + expect(result.total).toReportTimeSpent('total', 200, 50); }); test('Unfocused then focused then unfocused', () => { @@ -57,7 +76,7 @@ test('Unfocused then focused then unfocused', () => { ], }; const result = computeUsageSummary(state, 650); - expect(result).toReportTimeSpent('total', 200, 350); + expect(result.total).toReportTimeSpent('total', 200, 350); }); test('Focused then unfocused then focused', () => { @@ -69,49 +88,83 @@ test('Focused then unfocused then focused', () => { ], }; const result = computeUsageSummary(state, 650); - expect(result).toReportTimeSpent('total', 350, 200); + expect(result.total).toReportTimeSpent('total', 350, 200); }); test('Always focused plugin change', () => { const state: State = { timeline: [ {type: 'TIMELINE_START', time: 100, isFocused: true}, - {type: 'PLUGIN_SELECTED', time: 150, plugin: 'Layout'}, + { + type: 'PLUGIN_SELECTED', + time: 150, + pluginKey: layoutPluginKey, + pluginData, + }, ], }; const result = computeUsageSummary(state, 200); - expect(result).toReportTimeSpent('total', 100, 0); - expect(result).toReportTimeSpent('Layout', 50, 0); + expect(result.total).toReportTimeSpent('total', 100, 0); + expect(result.plugin[layoutPluginKey]).toReportTimeSpent('Layout', 50, 0); }); test('Focused then plugin change then unfocusd', () => { const state: State = { timeline: [ {type: 'TIMELINE_START', time: 100, isFocused: true}, - {type: 'PLUGIN_SELECTED', time: 150, plugin: 'Layout'}, + { + type: 'PLUGIN_SELECTED', + time: 150, + pluginKey: layoutPluginKey, + pluginData, + }, {type: 'WINDOW_FOCUS_CHANGE', time: 350, isFocused: false}, ], }; const result = computeUsageSummary(state, 650); - expect(result).toReportTimeSpent('total', 250, 300); - expect(result).toReportTimeSpent('Layout', 200, 300); + expect(result.total).toReportTimeSpent('total', 250, 300); + expect(result.plugin[layoutPluginKey]).toReportTimeSpent('Layout', 200, 300); }); test('Multiple plugin changes', () => { const state: State = { timeline: [ {type: 'TIMELINE_START', time: 100, isFocused: true}, - {type: 'PLUGIN_SELECTED', time: 150, plugin: 'Layout'}, - {type: 'PLUGIN_SELECTED', time: 350, plugin: 'Network'}, - {type: 'PLUGIN_SELECTED', time: 650, plugin: 'Layout'}, - {type: 'PLUGIN_SELECTED', time: 1050, plugin: 'Databases'}, + { + type: 'PLUGIN_SELECTED', + time: 150, + pluginKey: layoutPluginKey, + pluginData, + }, + { + type: 'PLUGIN_SELECTED', + time: 350, + pluginKey: networkPluginKey, + pluginData: pluginData2, + }, + { + type: 'PLUGIN_SELECTED', + time: 650, + pluginKey: layoutPluginKey, + pluginData, + }, + { + type: 'PLUGIN_SELECTED', + time: 1050, + pluginKey: databasesPluginKey, + pluginData: pluginData3, + }, ], }; const result = computeUsageSummary(state, 1550); - expect(result).toReportTimeSpent('total', 1450, 0); - expect(result).toReportTimeSpent('Layout', 600, 0); - expect(result).toReportTimeSpent('Network', 300, 0); - expect(result).toReportTimeSpent('Databases', 500, 0); + expect(result.total).toReportTimeSpent('total', 1450, 0); + expect(result.plugin[layoutPluginKey]).toReportTimeSpent('Layout', 600, 0); + expect(result.plugin[networkPluginKey]).toReportTimeSpent('Network', 300, 0); + expect(result.plugin[databasesPluginKey]).toReportTimeSpent( + 'Databases', + 500, + 0, + ); }); declare global { @@ -128,20 +181,27 @@ declare global { expect.extend({ toReportTimeSpent( - received: UsageSummary, + received: {focusedTime: number; unfocusedTime: number} | undefined, plugin: string, focusedTimeSpent: number, unfocusedTimeSpent: number, ) { - const focusedPass = received[plugin].focusedTime === focusedTimeSpent; - const unfocusedPass = received[plugin].unfocusedTime === unfocusedTimeSpent; + if (!received) { + return { + message: () => + `expected to have tracking element for plugin ${plugin}, but was not found`, + pass: false, + }; + } + const focusedPass = received.focusedTime === focusedTimeSpent; + const unfocusedPass = received.unfocusedTime === unfocusedTimeSpent; if (!focusedPass) { return { message: () => `expected ${JSON.stringify( received, )} to have focused time spent: ${focusedTimeSpent} for plugin ${plugin}, but was ${ - received[plugin]?.focusedTime + received.focusedTime }`, pass: false, }; @@ -153,7 +213,7 @@ expect.extend({ `expected ${JSON.stringify( received, )} to have unfocused time spent: ${unfocusedTimeSpent} for plugin ${plugin}, but was ${ - received[plugin]?.unfocusedTime + received.unfocusedTime }`, pass: false, }; diff --git a/desktop/app/src/dispatcher/tracking.tsx b/desktop/app/src/dispatcher/tracking.tsx index 10f8d1f47..329941161 100644 --- a/desktop/app/src/dispatcher/tracking.tsx +++ b/desktop/app/src/dispatcher/tracking.tsx @@ -22,23 +22,31 @@ import { clearTimeline, TrackingEvent, State as UsageTrackingState, + SelectedPluginData, } from '../reducers/usageTracking'; import produce from 'immer'; import BaseDevice from '../devices/BaseDevice'; -import {deconstructClientId} from '../utils/clientUtils'; +import {deconstructClientId, deconstructPluginKey} from '../utils/clientUtils'; import {getCPUUsage} from 'process'; +import {getPluginKey} from '../utils/pluginUtils'; const TIME_SPENT_EVENT = 'time-spent'; type UsageInterval = { - plugin: string | null; + pluginKey: string | null; + pluginData: SelectedPluginData | null; length: number; focused: boolean; }; export type UsageSummary = { total: {focusedTime: number; unfocusedTime: number}; - [pluginName: string]: {focusedTime: number; unfocusedTime: number}; + plugin: { + [pluginKey: string]: { + focusedTime: number; + unfocusedTime: number; + } & SelectedPluginData; + }; }; export const fpsEmitter = new EventEmitter(); @@ -152,8 +160,14 @@ export default (store: Store, logger: Logger) => { store.dispatch(clearTimeline(currentTime)); logger.track('usage', TIME_SPENT_EVENT, usageSummary.total); - for (const key of Object.keys(usageSummary)) { - logger.track('usage', TIME_SPENT_EVENT, usageSummary[key], key); + for (const key of Object.keys(usageSummary.plugin)) { + const keyParts = deconstructPluginKey(key); + logger.track( + 'usage', + TIME_SPENT_EVENT, + usageSummary.plugin[key], + keyParts.pluginName, + ); } Object.entries(state.connections.enabledPlugins).forEach( @@ -235,7 +249,8 @@ export function computeUsageSummary( const intervals: UsageInterval[] = []; let intervalStart = 0; let isFocused = false; - let selectedPlugin: string | null = null; + let pluginData: SelectedPluginData | null = null; + let pluginKey: string | null; function startInterval(event: TrackingEvent) { intervalStart = event.time; @@ -246,12 +261,13 @@ export function computeUsageSummary( isFocused = event.isFocused; } if (event.type === 'PLUGIN_SELECTED') { - selectedPlugin = event.plugin; + pluginKey = event.pluginKey; + pluginData = event.pluginData; } } function endInterval(time: number) { const length = time - intervalStart; - intervals.push({length, plugin: selectedPlugin, focused: isFocused}); + intervals.push({length, focused: isFocused, pluginKey, pluginData}); } for (const event of state.timeline) { @@ -273,16 +289,18 @@ export function computeUsageSummary( produce(acc, (draft) => { draft.total.focusedTime += x.focused ? x.length : 0; draft.total.unfocusedTime += x.focused ? 0 : x.length; - const pluginName = x.plugin ?? 'none'; - draft[pluginName] = draft[pluginName] ?? { + const pluginKey = x.pluginKey ?? getPluginKey(null, null, 'none'); + draft.plugin[pluginKey] = draft.plugin[pluginKey] ?? { focusedTime: 0, unfocusedTime: 0, + ...x.pluginData, }; - draft[pluginName].focusedTime += x.focused ? x.length : 0; - draft[pluginName].unfocusedTime += x.focused ? 0 : x.length; + draft.plugin[pluginKey].focusedTime += x.focused ? x.length : 0; + draft.plugin[pluginKey].unfocusedTime += x.focused ? 0 : x.length; }), { total: {focusedTime: 0, unfocusedTime: 0}, + plugin: {}, }, ); } diff --git a/desktop/app/src/reducers/usageTracking.tsx b/desktop/app/src/reducers/usageTracking.tsx index 4f62c53e2..d2b8a855f 100644 --- a/desktop/app/src/reducers/usageTracking.tsx +++ b/desktop/app/src/reducers/usageTracking.tsx @@ -10,6 +10,19 @@ import {produce} from 'immer'; import {remote} from 'electron'; import {Actions} from './'; +import {getPluginKey} from '../utils/pluginUtils'; +import {deconstructClientId} from '../utils/clientUtils'; + +export type SelectedPluginData = { + plugin: string | null; + app: string | null; + os: string | null; + device: string | null; + deviceName: string | null; + deviceSerial: string | null; + deviceType: string | null; + archived: boolean | null; +}; export type TrackingEvent = | { @@ -17,7 +30,12 @@ export type TrackingEvent = time: number; isFocused: boolean; } - | {type: 'PLUGIN_SELECTED'; time: number; plugin: string | null} + | { + type: 'PLUGIN_SELECTED'; + pluginKey: string | null; + pluginData: SelectedPluginData | null; + time: number; + } | {type: 'TIMELINE_START'; time: number; isFocused: boolean}; export type State = { @@ -65,10 +83,30 @@ export default function reducer( }); } else if (action.type === 'SELECT_PLUGIN') { return produce(state, (draft) => { + const selectedApp = action.payload.selectedApp; + const clientIdParts = selectedApp + ? deconstructClientId(selectedApp) + : null; draft.timeline.push({ type: 'PLUGIN_SELECTED', time: action.payload.time, - plugin: action.payload.selectedPlugin || null, + pluginKey: action.payload.selectedPlugin + ? getPluginKey( + action.payload.selectedApp, + action.payload.selectedDevice, + action.payload.selectedPlugin, + ) + : null, + pluginData: { + plugin: action.payload.selectedPlugin || null, + app: clientIdParts?.app || null, + device: action.payload.selectedDevice?.title || null, + deviceName: clientIdParts?.device || null, + deviceSerial: action.payload.selectedDevice?.serial || null, + deviceType: action.payload.selectedDevice?.deviceType || null, + os: action.payload.selectedDevice?.os || null, + archived: action.payload.selectedDevice?.isArchived || false, + }, }); }); }