Enhance time-spent tracking

Summary:
Previously, at 1-minute intervals, if the flipper window was focused on, it would report the currently active plugin.
We'd sum all those "ping" events and that would approximate the number of full minutes spent in total across all users.
It's quite coarse grained, if you're focused on the window for 30 seconds, there's a 50% change your ping will get used.
While being reasonable across many users, it doesn't allow analysis like how many plugins do people typically use in a session, because we probably won't see all the plugins they use.

New approach, for every minute flipper is open, report the focused and unfocused time spent in each plugin, as well as the total across all plugins.
This should give us the previous data but with much more precision.

Should be especially helpful for plugins with low numbers of users, you typically interact with emulators while using a plugin, so it's not continually in focus, so you miss a lot of usage events.

enhance_bladerunner

Reviewed By: nikoant

Differential Revision: D19392796

fbshipit-source-id: af9244e993edff9b381144ca587c3a77fdf8c98a
This commit is contained in:
John Knox
2020-01-14 10:25:52 -08:00
committed by Facebook Github Bot
parent 84302f109b
commit a96931c43f
9 changed files with 380 additions and 10 deletions

View File

@@ -90,7 +90,6 @@ type BooleanActionType =
| 'leftSidebarVisible'
| 'rightSidebarVisible'
| 'rightSidebarAvailable'
| 'windowIsFocused'
| 'downloadingImportData';
export type Action =
@@ -98,6 +97,10 @@ export type Action =
type: BooleanActionType;
payload?: boolean;
}
| {
type: 'windowIsFocused';
payload: {isFocused: boolean; time: number};
}
| {
type: 'SET_ACTIVE_SHEET';
payload: ActiveSheet;
@@ -169,6 +172,7 @@ export const initialState: () => State = () => ({
},
statusMessages: [],
xcodeCommandLineToolsDetected: false,
trackingTimeline: [],
});
function statusMessage(sender: string, msg: string): string {
@@ -191,7 +195,6 @@ export default function reducer(
action.type === 'leftSidebarVisible' ||
action.type === 'rightSidebarVisible' ||
action.type === 'rightSidebarAvailable' ||
action.type === 'windowIsFocused' ||
action.type === 'downloadingImportData'
) {
const newValue =
@@ -208,6 +211,11 @@ export default function reducer(
[action.type]: newValue,
};
}
} else if (action.type === 'windowIsFocused') {
return {
...state,
windowIsFocused: action.payload.isFocused,
};
} else if (action.type === 'SET_ACTIVE_SHEET') {
return {
...state,

View File

@@ -88,6 +88,7 @@ export type Action =
selectedApp?: null | string;
deepLinkPayload: null | string;
selectedDevice?: null | BaseDevice;
time: number;
};
}
| {
@@ -461,9 +462,10 @@ export const selectPlugin = (payload: {
selectedApp?: null | string;
selectedDevice?: BaseDevice | null;
deepLinkPayload: null | string;
time?: number;
}): Action => ({
type: 'SELECT_PLUGIN',
payload,
payload: {...payload, time: payload.time ?? Date.now()},
});
export const starPlugin = (payload: {

View File

@@ -52,6 +52,10 @@ import healthchecks, {
Action as HealthcheckAction,
State as HealthcheckState,
} from './healthchecks';
import usageTracking, {
Action as TrackingAction,
State as TrackingState,
} from './usageTracking';
import user, {State as UserState, Action as UserAction} from './user';
import JsonFileStorage from '../utils/jsonFileReduxPersistStorage';
import LauncherSettingsStorage from '../utils/launcherSettingsStorage';
@@ -78,6 +82,7 @@ export type Actions =
| SupportFormAction
| PluginManagerAction
| HealthcheckAction
| TrackingAction
| {type: 'INIT'};
export type State = {
@@ -93,6 +98,7 @@ export type State = {
supportForm: SupportFormState;
pluginManager: PluginManagerState;
healthchecks: HealthcheckState & PersistPartial;
usageTracking: TrackingState;
};
export type Store = ReduxStore<State, Actions>;
@@ -167,4 +173,5 @@ export default combineReducers<State, Actions>({
},
healthchecks,
),
usageTracking,
});

View File

@@ -0,0 +1,86 @@
/**
* 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 {produce} from 'immer';
import {remote} from 'electron';
import {Actions} from './';
export type TrackingEvent =
| {
type: 'WINDOW_FOCUS_CHANGE';
time: number;
isFocused: boolean;
}
| {type: 'PLUGIN_SELECTED'; time: number; plugin: string | null}
| {type: 'TIMELINE_START'; time: number; isFocused: boolean};
export type State = {
timeline: TrackingEvent[];
};
const INITAL_STATE: State = {
timeline: [
{
type: 'TIMELINE_START',
time: Date.now(),
isFocused: remote.getCurrentWindow().isFocused(),
},
],
};
export type Action =
| {
type: 'windowIsFocused';
payload: {isFocused: boolean; time: number};
}
| {type: 'CLEAR_TIMELINE'; payload: {time: number; isFocused: boolean}};
export default function reducer(
state: State = INITAL_STATE,
action: Actions,
): State {
if (action.type === 'CLEAR_TIMELINE') {
return {
...state,
timeline: [
{
type: 'TIMELINE_START',
time: action.payload.time,
isFocused: action.payload.isFocused,
},
],
};
} else if (action.type === 'windowIsFocused') {
return produce(state, draft => {
draft.timeline.push({
type: 'WINDOW_FOCUS_CHANGE',
time: action.payload.time,
isFocused: action.payload.isFocused,
});
});
} else if (action.type === 'SELECT_PLUGIN') {
return produce(state, draft => {
draft.timeline.push({
type: 'PLUGIN_SELECTED',
time: action.payload.time,
plugin: action.payload.selectedPlugin || null,
});
});
}
return state;
}
export function clearTimeline(time: number): Action {
return {
type: 'CLEAR_TIMELINE',
payload: {
time,
isFocused: remote.getCurrentWindow().isFocused(),
},
};
}