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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
c007d74af9
commit
e1f6f770cd
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user