Skip to content

Commit

Permalink
feat(app): Add robot pipettes, versions, FFs to mixpanel and intercom (
Browse files Browse the repository at this point in the history
…#3059)

Closes #3009, closes #3010
  • Loading branch information
mcous authored Feb 19, 2019
1 parent 886bd68 commit de4a15f
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 52 deletions.
89 changes: 78 additions & 11 deletions app/src/analytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const store = createStore(reducer, middleware)
For a given Redux action, add a `case` to the `action.type` switch in [`app/src/analytics/make-event.js`](./make-event.js). `makeEvent` will be passed the full state and the action. Any new case should return either `null` or an object `{name: string, properties: {}}`

```js
export default function makeEvent (
export default function makeEvent(
action: Action,
nextState: State,
prevState: State
Expand All @@ -44,16 +44,72 @@ export default function makeEvent (

## events

| name | redux action | payload |
| ------------------------ | ------------------------------ | --------------------------------------- |
| `robotConnect` | `robot:CONNECT_RESPONSE` | success, method, error |
| `protocolUploadRequest` | `protocol:UPLOAD` | protocol data |
| `protocolUploadResponse` | `robot:SESSION_RESPONSE/ERROR` | protocol data, success, error |
| `runStart` | `robot:RUN` | protocol data |
| `runFinish` | `robot:RUN_RESPONSE` | protocol data, success, error, run time |
| `runPause` | `robot:PAUSE` | protocol, run time data |
| `runResume` | `robot:RESUME` | protocol, run time data |
| `runCancel` | `robot:CANCEL` | protocol, run time data |
### event names and payloads

#### robotConnect

- Redux action: `robot:CONNECT_RESPONSE`
- Payload:
- success: boolean
- method: 'usb' | 'wifi'
- error: string

#### protocolUploadRequest

- Redux action: `protocol:UPLOAD`
- Payload:
- ...protocol data (see below)
- ...robot data (see below)

#### protocolUploadResponse

- Redux action:
- `robot:SESSION_RESPONSE`
- `robot:SESSION_ERROR`
- Payload:
- ...protocol data (see below)
- ...robot data (see below)
- success: boolean
- error: string

#### runStart

- Redux action: `robot:RUN`
- Payload:

- ...protocol data (see below)
- ...robot data (see below)

#### runFinish

- Redux action: `robot:RUN_RESPONSE`
- Payload:
- ...protocol data (see below)
- ...robot data (see below)
- success: boolean
- error: string
- runTime: number

#### runPause

- Redux action: `robot:PAUSE`
- Payload:
- ...protocol data (see below)
- runTime: number

#### runResume

- Redux action: `robot:RESUME`
- Payload:
- ...protocol data (see below)
- runTime: number

#### runCancel

- Redux action: `robot:CANCEL`
- Payload:
- ...protocol data (see below)
- runTime: number

### hashing

Expand All @@ -69,4 +125,15 @@ Some payload fields are [hashed][] for user anonymity while preserving our abili
- Protocol `metadata.author` (hashed for anonymity)
- Protocol `metadata.protocolText` (hashed for anonymity)

### robot data sent

- Pipette models
- `robotLeftPipette: string`
- `robotRightPipette: string`
- Software versions
- `robotApiServerVersion: string`
- `robotSmoothieVersion: string`
- Feature flags
- `robotFF_someFeatureFlagId: boolean`

[hashed]: https://en.wikipedia.org/wiki/Hash_function
2 changes: 2 additions & 0 deletions app/src/analytics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {State, Action, ThunkAction, Middleware} from '../types'
import type {Config} from '../config'
import type {AnalyticsEvent} from './types'

export * from './selectors'

type AnalyticsConfig = $PropertyType<Config, 'analytics'>

const log = createLogger(__filename)
Expand Down
21 changes: 17 additions & 4 deletions app/src/analytics/make-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import createLogger from '../logger'
import {selectors as robotSelectors} from '../robot'
import {getConnectedRobot} from '../discovery'
import {getProtocolAnalyticsData} from './selectors'
import {getProtocolAnalyticsData, getRobotAnalyticsData} from './selectors'

import type {State, Action} from '../types'
import type {AnalyticsEvent} from './types'
Expand Down Expand Up @@ -37,7 +37,10 @@ export default function makeEvent (
case 'protocol:UPLOAD': {
return getProtocolAnalyticsData(nextState).then(data => ({
name: 'protocolUploadRequest',
properties: data,
properties: {
...data,
...getRobotAnalyticsData(nextState),
},
}))
}

Expand All @@ -52,6 +55,7 @@ export default function makeEvent (
name: 'protocolUploadResponse',
properties: {
...data,
...getRobotAnalyticsData(nextState),
success: actionType === 'robot:SESSION_RESPONSE',
error: (actionPayload.error && actionPayload.error.message) || '',
},
Expand All @@ -62,7 +66,10 @@ export default function makeEvent (
case 'robot:RUN': {
return getProtocolAnalyticsData(nextState).then(data => ({
name: 'runStart',
properties: data,
properties: {
...data,
...getRobotAnalyticsData(nextState),
},
}))
}

Expand All @@ -77,7 +84,13 @@ export default function makeEvent (

return getProtocolAnalyticsData(nextState).then(data => ({
name: 'runFinish',
properties: {...data, runTime, success, error},
properties: {
...data,
...getRobotAnalyticsData(nextState),
runTime,
success,
error,
},
}))
}

Expand Down
51 changes: 50 additions & 1 deletion app/src/analytics/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,28 @@ import {
getProtocolContents,
} from '../protocol'

import {
getConnectedRobot,
getRobotApiVersion,
getRobotFirmwareVersion,
} from '../discovery'

import {
getRobotApiState,
getSettingsRequest,
getPipettesRequest,
} from '../http-api-client'

import hash from './hash'

import type {OutputSelector} from 'reselect'
import type {State} from '../types'
import type {ProtocolAnalyticsData} from './types'
import type {ProtocolAnalyticsData, RobotAnalyticsData} from './types'

type ProtocolDataSelector = OutputSelector<State, void, ProtocolAnalyticsData>

export const FF_PREFIX = 'robotFF_'

const _getUnhashedProtocolAnalyticsData: ProtocolDataSelector = createSelector(
getProtocolType,
getProtocolCreatorApp,
Expand Down Expand Up @@ -50,3 +64,38 @@ export function getProtocolAnalyticsData (
return {...data, protocolAuthor, protocolText}
})
}

export function getRobotAnalyticsData (state: State): RobotAnalyticsData | null {
const robot = getConnectedRobot(state)

if (robot) {
const api = getRobotApiState(state, robot)
const settingsRequest = getSettingsRequest(api)
const pipettesRequest = getPipettesRequest(api)
const settings = settingsRequest.response
? settingsRequest.response.settings
: []

const pipettes = pipettesRequest.response
? {
left: pipettesRequest.response.left.model,
right: pipettesRequest.response.right.model,
}
: {left: null, right: null}

return settings.reduce(
(result, setting) => ({
...result,
[`${FF_PREFIX}${setting.id}`]: !!setting.value,
}),
{
robotApiServerVersion: getRobotApiVersion(robot) || '',
robotSmoothieVersion: getRobotFirmwareVersion(robot) || '',
robotLeftPipette: pipettes.left || '',
robotRightPipette: pipettes.right || '',
}
)
}

return null
}
11 changes: 11 additions & 0 deletions app/src/analytics/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export type ProtocolAnalyticsData = {
protocolText: string,
}

export type RobotAnalyticsData = {
robotApiServerVersion: string,
robotSmoothieVersion: string,
robotLeftPipette: string,
robotRightPipette: string,

// feaure flags
// e.g. robotFF_settingName
[ffName: string]: boolean,
}

export type AnalyticsEvent = {|
name: string,
properties: {},
Expand Down
2 changes: 2 additions & 0 deletions app/src/http-api-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export type Action =
| ServerAction
| SettingsAction

export {getRobotApiState} from './reducer'

export {
startDeckCalibration,
deckCalibrationCommand,
Expand Down
17 changes: 12 additions & 5 deletions app/src/http-api-client/pipettes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {OutputSelector} from 'reselect'
import type {State, ThunkPromiseAction} from '../types'
import type {BaseRobot, RobotService} from '../robot'
import type {ApiCall, ApiRequestError} from './types'
import type {RobotApiState} from './reducer'
import type {ApiAction} from './actions'
import type {MotorAxis} from './motors'

Expand Down Expand Up @@ -44,7 +45,7 @@ export function fetchPipettes (
let path = PIPETTES
if (refresh) path += '?refresh=true'

return (dispatch) => {
return dispatch => {
dispatch(apiRequest(robot, path, null))

return client(robot, 'GET', path)
Expand All @@ -57,10 +58,16 @@ export function fetchPipettes (
}

export const makeGetRobotPipettes = () => {
const selector: OutputSelector<State, BaseRobot, RobotPipettes> = createSelector(
getRobotApiState,
(state) => state[PIPETTES] || {inProgress: false}
)
const selector: OutputSelector<State,
BaseRobot,
RobotPipettes> = createSelector(
getRobotApiState,
getPipettesRequest
)

return selector
}

export function getPipettesRequest (state: RobotApiState): RobotPipettes {
return state[PIPETTES] || {inProgress: false}
}
34 changes: 27 additions & 7 deletions app/src/http-api-client/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {ResetState} from './reset'
import type {SettingsState} from './settings'
import type {NetworkingState} from './networking'

type RobotApiState = {|
export type RobotApiState = {|
...HealthState,
...PipettesState,
...ModulesState,
Expand All @@ -34,7 +34,10 @@ export default function apiReducer (
switch (action.type) {
case 'api:REQUEST': {
const {request} = action.payload
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action)
const {name, path, stateByName, stateByPath} = getUpdateInfo(
state,
action
)

return {
...state,
Expand All @@ -47,7 +50,10 @@ export default function apiReducer (

case 'api:SUCCESS': {
const {response} = action.payload
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action)
const {name, path, stateByName, stateByPath} = getUpdateInfo(
state,
action
)

return {
...state,
Expand All @@ -60,7 +66,10 @@ export default function apiReducer (

case 'api:FAILURE': {
const {error} = action.payload
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action)
const {name, path, stateByName, stateByPath} = getUpdateInfo(
state,
action
)
if (!stateByPath || !stateByPath.inProgress) return state

return {
Expand All @@ -73,13 +82,21 @@ export default function apiReducer (
}

case 'api:CLEAR_RESPONSE': {
const {name, path, stateByName, stateByPath} = getUpdateInfo(state, action)
const {name, path, stateByName, stateByPath} = getUpdateInfo(
state,
action
)

return {
...state,
[name]: {
...stateByName,
[path]: {...stateByPath, response: null, inProgress: false, error: null},
[path]: {
...stateByPath,
response: null,
inProgress: false,
error: null,
},
},
}
}
Expand Down Expand Up @@ -113,7 +130,10 @@ export function getRobotApiState (
}

function getUpdateInfo (state: ApiState, action: *): * {
const {path, robot: {name}} = action.payload
const {
path,
robot: {name},
} = action.payload
const stateByName = state[name] || {}
// $FlowFixMe: type RobotApiState properly
const stateByPath = stateByName[path] || {}
Expand Down
Loading

0 comments on commit de4a15f

Please sign in to comment.