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