From 14912834829c2e99f8bff06210bebfc5ee06064a Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 24 Apr 2017 16:33:01 +0200 Subject: [PATCH 1/6] Added mixpanel tracking for bunch of events. --- client/app/scripts/actions/app-actions.js | 30 ++++++++- .../components/metric-selector-item.js | 17 ++++- client/app/scripts/components/search.js | 13 +++- client/app/scripts/components/topologies.js | 13 +++- .../scripts/components/view-mode-selector.js | 66 +++++++++++-------- client/app/scripts/utils/tracking-utils.js | 13 ++++ 6 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 client/app/scripts/utils/tracking-utils.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 4085b8e7ff..d20c9dd22e 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -24,6 +24,7 @@ import { import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { availableMetricTypesSelector, selectedMetricTypeSelector, @@ -200,6 +201,13 @@ export function changeTopologyOption(option, value, topologyId, addOrRemove) { // update all request workers with new options resetUpdateBuffer(); const state = getState(); + trackMixpanelEvent('scope.topology.option.click', { + option, + value, + layout: state.get('topologyViewMode'), + topologyId: state.getIn(['currentTopology', 'id']), + parentTopologyId: state.getIn(['currentTopology', 'parentId']), + }); getTopologies(activeTopologyOptionsSelector(state), dispatch); getNodesDelta( getCurrentTopologyUrl(state), @@ -257,7 +265,11 @@ export function clickDownloadGraph() { } export function clickForceRelayout() { - return (dispatch) => { + return (dispatch, getState) => { + const state = getState(); + trackMixpanelEvent('scope.layout.refresh.click', { + layout: state.get('topologyViewMode'), + }); dispatch({ type: ActionTypes.CLICK_FORCE_RELAYOUT, forceRelayout: true @@ -335,6 +347,11 @@ export function clickNode(nodeId, label, origin) { }); updateRoute(getState); const state = getState(); + trackMixpanelEvent('scope.node.click', { + layout: state.get('topologyViewMode'), + topologyId: state.getIn(['currentTopology', 'id']), + parentTopologyId: state.getIn(['currentTopology', 'parentId']), + }); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), @@ -362,6 +379,11 @@ export function clickRelative(nodeId, topologyId, label, origin) { }); updateRoute(getState); const state = getState(); + trackMixpanelEvent('scope.node.relative.click', { + layout: state.get('topologyViewMode'), + topologyId: state.getIn(['currentTopology', 'id']), + parentTopologyId: state.getIn(['currentTopology', 'parentId']), + }); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), @@ -511,6 +533,12 @@ export function hitEnter() { if (state.get('searchFocused')) { const query = state.get('searchQuery'); if (query && parseQuery(query)) { + trackMixpanelEvent('scope.search.query.pin', { + query, + layout: state.get('topologyViewMode'), + topologyId: state.getIn(['currentTopology', 'id']), + parentTopologyId: state.getIn(['currentTopology', 'parentId']), + }); dispatch({ type: ActionTypes.PIN_SEARCH, query diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 11f2cb2279..15dc30a68c 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { hoverMetric, pinMetric, unpinMetric } from '../actions/app-actions'; import { selectedMetricTypeSelector } from '../selectors/node-metric'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; class MetricSelectorItem extends React.Component { @@ -24,8 +25,20 @@ class MetricSelectorItem extends React.Component { const pinnedMetricType = this.props.pinnedMetricType; if (metricType !== pinnedMetricType) { + trackMixpanelEvent('scope.metric.resource.pin', { + metricType, + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); this.props.pinMetric(metricType); } else { + trackMixpanelEvent('scope.metric.resource.unpin', { + metricType, + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); this.props.unpinMetric(); } } @@ -54,8 +67,10 @@ class MetricSelectorItem extends React.Component { function mapStateToProps(state) { return { - selectedMetricType: selectedMetricTypeSelector(state), + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), pinnedMetricType: state.get('pinnedMetricType'), + selectedMetricType: selectedMetricTypeSelector(state), }; } diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 6bcf73e008..fdd6eb4d1b 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -8,6 +8,7 @@ import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; import { slugify } from '../utils/string-utils'; import { isTopologyEmpty } from '../utils/topology-utils'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import SearchItem from './search-item'; @@ -71,7 +72,7 @@ class Search extends React.Component { if (this.state.value && value === '') { value = null; } - this.setState({value}); + this.setState({ value }); this.doSearch(inputValue); } @@ -80,6 +81,14 @@ class Search extends React.Component { } doSearch(value) { + if (value !== '') { + trackMixpanelEvent('scope.search.query.change', { + query: value, + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } this.props.doSearch(value); } @@ -155,8 +164,10 @@ class Search extends React.Component { export default connect( state => ({ nodes: state.get('nodes'), + topologyViewMode: state.get('topologyViewMode'), isResourceViewMode: isResourceViewModeSelector(state), isTopologyEmpty: isTopologyEmpty(state), + currentTopology: state.get('currentTopology'), topologiesLoaded: state.get('topologiesLoaded'), pinnedSearches: state.get('pinnedSearches'), searchFocused: state.get('searchFocused'), diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index a5060ac990..880141b68b 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import classnames from 'classnames'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; import { clickTopology } from '../actions/app-actions'; @@ -25,8 +26,12 @@ class Topologies extends React.Component { this.onTopologyClick = this.onTopologyClick.bind(this); } - onTopologyClick(ev) { + onTopologyClick(ev, topology) { ev.preventDefault(); + trackMixpanelEvent('scope.topology.selector.click', { + topologyId: topology.get('id'), + parentTopologyId: topology.get('parentId'), + }); this.props.clickTopology(ev.currentTarget.getAttribute('rel')); } @@ -44,7 +49,7 @@ class Topologies extends React.Component { return (
+ onClick={ev => this.onTopologyClick(ev, subTopology)}>
{subTopology.get('name')}
@@ -65,7 +70,9 @@ class Topologies extends React.Component { return (
-
+
this.onTopologyClick(ev, topology)}>
{topology.get('name')}
diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index 4e8cfdaa98..e8afc26af5 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -3,32 +3,18 @@ import { connect } from 'react-redux'; import classNames from 'classnames'; import MetricSelector from './metric-selector'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { availableMetricsSelector } from '../selectors/node-metric'; +import { isResourceViewModeSelector } from '../selectors/topology'; import { - isGraphViewModeSelector, - isTableViewModeSelector, - isResourceViewModeSelector, -} from '../selectors/topology'; + GRAPH_VIEW_MODE, + TABLE_VIEW_MODE, + RESOURCE_VIEW_MODE, +} from '../constants/naming'; -const Item = (icons, label, isSelected, onClick, isEnabled = true) => { - const className = classNames('view-mode-selector-action', { - 'view-mode-selector-action-selected': isSelected, - }); - return ( -
- - {label} -
- ); -}; - class ViewModeSelector extends React.Component { componentWillReceiveProps(nextProps) { if (nextProps.isResourceViewMode && !nextProps.hasResourceView) { @@ -36,16 +22,42 @@ class ViewModeSelector extends React.Component { } } + renderItem(icons, label, viewMode, setViewModeAction, isEnabled = true) { + const isSelected = (this.props.topologyViewMode === viewMode); + const className = classNames('view-mode-selector-action', { + 'view-mode-selector-action-selected': isSelected, + }); + const onClick = () => { + trackMixpanelEvent('scope.layout.selector.click', { + layout: viewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + setViewModeAction(); + }; + + return ( +
+ + {label} +
+ ); + } + render() { - const { isGraphViewMode, isTableViewMode, isResourceViewMode, hasResourceView } = this.props; + const { hasResourceView } = this.props; return (
- {Item('fa fa-share-alt', 'Graph', isGraphViewMode, this.props.setGraphView)} - {Item('fa fa-table', 'Table', isTableViewMode, this.props.setTableView)} - {Item('fa fa-bar-chart', 'Resources', isResourceViewMode, this.props.setResourceView, - hasResourceView)} + {this.renderItem('fa fa-share-alt', 'Graph', GRAPH_VIEW_MODE, this.props.setGraphView)} + {this.renderItem('fa fa-table', 'Table', TABLE_VIEW_MODE, this.props.setTableView)} + {this.renderItem('fa fa-bar-chart', 'Resources', RESOURCE_VIEW_MODE, + this.props.setResourceView, hasResourceView)}
@@ -55,11 +67,11 @@ class ViewModeSelector extends React.Component { function mapStateToProps(state) { return { - isGraphViewMode: isGraphViewModeSelector(state), - isTableViewMode: isTableViewModeSelector(state), isResourceViewMode: isResourceViewModeSelector(state), hasResourceView: !layersTopologyIdsSelector(state).isEmpty(), showingMetricsSelector: availableMetricsSelector(state).count() > 0, + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), }; } diff --git a/client/app/scripts/utils/tracking-utils.js b/client/app/scripts/utils/tracking-utils.js new file mode 100644 index 0000000000..3464436dcf --- /dev/null +++ b/client/app/scripts/utils/tracking-utils.js @@ -0,0 +1,13 @@ +import debug from 'debug'; + +const log = debug('service:tracking'); + +// Track mixpanel events only if Scope is running inside of Weave Cloud. +export function trackMixpanelEvent(name, props) { + if (window.mixpanel && process.env.WEAVE_CLOUD) { + window.mixpanel.track(name, props); + log('trackMixpanelEvent', name, props); + } else { + log('trackMixpanelEvent', name, props); + } +} From 433136c680827de66e33bff0b4f0abf4319927b2 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 24 Apr 2017 18:13:09 +0200 Subject: [PATCH 2/6] Changed hitEnter action to pinSearch. --- client/app/scripts/actions/app-actions.js | 34 ++++++------------- client/app/scripts/components/app.js | 11 +++--- .../components/metric-selector-item.js | 23 ++++++------- client/app/scripts/components/search.js | 22 ++++++++++-- client/app/scripts/constants/key-codes.js | 4 +++ 5 files changed, 48 insertions(+), 46 deletions(-) create mode 100644 client/app/scripts/constants/key-codes.js diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index d20c9dd22e..dd99996f1d 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -4,7 +4,6 @@ import ActionTypes from '../constants/action-types'; import { saveGraph } from '../utils/file-utils'; import { modulo } from '../utils/math-utils'; import { updateRoute } from '../utils/router-utils'; -import { parseQuery } from '../utils/search-utils'; import { bufferDeltaUpdate, resumeUpdate, @@ -174,6 +173,16 @@ export function pinNextMetric(delta) { }; } +export function pinSearch() { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.PIN_SEARCH, + query: getState().get('searchQuery'), + }); + updateRoute(getState); + }; +} + export function unpinSearch(query) { return (dispatch, getState) => { dispatch({ @@ -526,29 +535,6 @@ export function hitBackspace() { }; } -export function hitEnter() { - return (dispatch, getState) => { - const state = getState(); - // pin query based on current search field - if (state.get('searchFocused')) { - const query = state.get('searchQuery'); - if (query && parseQuery(query)) { - trackMixpanelEvent('scope.search.query.pin', { - query, - layout: state.get('topologyViewMode'), - topologyId: state.getIn(['currentTopology', 'id']), - parentTopologyId: state.getIn(['currentTopology', 'parentId']), - }); - dispatch({ - type: ActionTypes.PIN_SEARCH, - query - }); - updateRoute(getState); - } - } - }; -} - export function hitEsc() { return (dispatch, getState) => { const state = getState(); diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index a98bbbdcb6..8564693c1e 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -16,7 +16,6 @@ import { focusSearch, pinNextMetric, hitBackspace, - hitEnter, hitEsc, unpinMetric, toggleHelp, @@ -38,11 +37,11 @@ import { isTableViewModeSelector, isGraphViewModeSelector, } from '../selectors/topology'; +import { + BACKSPACE_KEY_CODE, + ESC_KEY_CODE, +} from '../constants/key-codes'; - -const BACKSPACE_KEY_CODE = 8; -const ENTER_KEY_CODE = 13; -const ESC_KEY_CODE = 27; const keyPressLog = debug('scope:app-key-press'); class App extends React.Component { @@ -79,8 +78,6 @@ class App extends React.Component { // don't get esc in onKeyPress if (ev.keyCode === ESC_KEY_CODE) { this.props.dispatch(hitEsc()); - } else if (ev.keyCode === ENTER_KEY_CODE) { - this.props.dispatch(hitEnter()); } else if (ev.keyCode === BACKSPACE_KEY_CODE) { this.props.dispatch(hitBackspace()); } else if (ev.code === 'KeyD' && ev.ctrlKey && !showingTerminal) { diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 15dc30a68c..57f8ef03e4 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -15,6 +15,15 @@ class MetricSelectorItem extends React.Component { this.onMouseClick = this.onMouseClick.bind(this); } + trackEvent(eventName) { + trackMixpanelEvent(eventName, { + metricType: this.props.metric.get('label'), + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + onMouseOver() { const metricType = this.props.metric.get('label'); this.props.hoverMetric(metricType); @@ -25,20 +34,10 @@ class MetricSelectorItem extends React.Component { const pinnedMetricType = this.props.pinnedMetricType; if (metricType !== pinnedMetricType) { - trackMixpanelEvent('scope.metric.resource.pin', { - metricType, - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); + this.trackEvent('scope.metric.resource.pin'); this.props.pinMetric(metricType); } else { - trackMixpanelEvent('scope.metric.resource.unpin', { - metricType, - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); + this.trackEvent('scope.metric.resource.unpin'); this.props.unpinMetric(); } } diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index fdd6eb4d1b..293de76539 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -3,13 +3,15 @@ import { connect } from 'react-redux'; import classnames from 'classnames'; import { debounce } from 'lodash'; -import { blurSearch, doSearch, focusSearch, toggleHelp } from '../actions/app-actions'; +import { blurSearch, doSearch, focusSearch, pinSearch, toggleHelp } from '../actions/app-actions'; import { searchMatchCountByTopologySelector } from '../selectors/search'; import { isResourceViewModeSelector } from '../selectors/topology'; import { slugify } from '../utils/string-utils'; +import { parseQuery } from '../utils/search-utils'; import { isTopologyEmpty } from '../utils/topology-utils'; import { trackMixpanelEvent } from '../utils/tracking-utils'; import SearchItem from './search-item'; +import { ENTER_KEY_CODE } from '../constants/key-codes'; function shortenHintLabel(text) { @@ -49,6 +51,7 @@ class Search extends React.Component { super(props, context); this.handleBlur = this.handleBlur.bind(this); this.handleChange = this.handleChange.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); this.handleFocus = this.handleFocus.bind(this); this.saveQueryInputRef = this.saveQueryInputRef.bind(this); this.doSearch = debounce(this.doSearch.bind(this), 200); @@ -76,6 +79,19 @@ class Search extends React.Component { this.doSearch(inputValue); } + handleKeyUp(ev) { + // If the search query is parsable, pin it when ENTER key is hit. + if (ev.keyCode === ENTER_KEY_CODE && parseQuery(this.props.searchQuery)) { + trackMixpanelEvent('scope.search.query.pin', { + query: this.props.searchQuery, + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + this.props.pinSearch(); + } + } + handleFocus() { this.props.focusSearch(); } @@ -139,7 +155,7 @@ class Search extends React.Component { .map(query => )}
@@ -174,5 +190,5 @@ export default connect( searchQuery: state.get('searchQuery'), searchMatchCountByTopology: searchMatchCountByTopologySelector(state), }), - { blurSearch, doSearch, focusSearch, toggleHelp } + { blurSearch, doSearch, focusSearch, pinSearch, toggleHelp } )(Search); diff --git a/client/app/scripts/constants/key-codes.js b/client/app/scripts/constants/key-codes.js new file mode 100644 index 0000000000..7aa6b15e69 --- /dev/null +++ b/client/app/scripts/constants/key-codes.js @@ -0,0 +1,4 @@ + +export const BACKSPACE_KEY_CODE = 8; +export const ENTER_KEY_CODE = 13; +export const ESC_KEY_CODE = 27; From cf0ee15bbaa42695faddf0734b3ed632535b0d2e Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 24 Apr 2017 18:40:28 +0200 Subject: [PATCH 3/6] Moved all the event tracking out of app-actions.js --- client/app/scripts/actions/app-actions.js | 24 +------------- client/app/scripts/charts/node.js | 10 +++++- client/app/scripts/charts/nodes-grid.js | 9 ++++- client/app/scripts/components/footer.js | 33 +++++++++++++++---- .../node-details-relatives-link.js | 6 ++++ .../node-details-table-node-link.js | 6 ++++ .../scripts/components/topology-options.js | 27 ++++++++++++--- client/app/scripts/utils/tracking-utils.js | 1 - 8 files changed, 79 insertions(+), 37 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index dd99996f1d..1c8a96fc16 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -23,7 +23,6 @@ import { import { getCurrentTopologyUrl } from '../utils/topology-utils'; import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; -import { trackMixpanelEvent } from '../utils/tracking-utils'; import { availableMetricTypesSelector, selectedMetricTypeSelector, @@ -210,13 +209,6 @@ export function changeTopologyOption(option, value, topologyId, addOrRemove) { // update all request workers with new options resetUpdateBuffer(); const state = getState(); - trackMixpanelEvent('scope.topology.option.click', { - option, - value, - layout: state.get('topologyViewMode'), - topologyId: state.getIn(['currentTopology', 'id']), - parentTopologyId: state.getIn(['currentTopology', 'parentId']), - }); getTopologies(activeTopologyOptionsSelector(state), dispatch); getNodesDelta( getCurrentTopologyUrl(state), @@ -274,11 +266,7 @@ export function clickDownloadGraph() { } export function clickForceRelayout() { - return (dispatch, getState) => { - const state = getState(); - trackMixpanelEvent('scope.layout.refresh.click', { - layout: state.get('topologyViewMode'), - }); + return (dispatch) => { dispatch({ type: ActionTypes.CLICK_FORCE_RELAYOUT, forceRelayout: true @@ -356,11 +344,6 @@ export function clickNode(nodeId, label, origin) { }); updateRoute(getState); const state = getState(); - trackMixpanelEvent('scope.node.click', { - layout: state.get('topologyViewMode'), - topologyId: state.getIn(['currentTopology', 'id']), - parentTopologyId: state.getIn(['currentTopology', 'parentId']), - }); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), @@ -388,11 +371,6 @@ export function clickRelative(nodeId, topologyId, label, origin) { }); updateRoute(getState); const state = getState(); - trackMixpanelEvent('scope.node.relative.click', { - layout: state.get('topologyViewMode'), - topologyId: state.getIn(['currentTopology', 'id']), - parentTopologyId: state.getIn(['currentTopology', 'parentId']), - }); getNodeDetails( state.get('topologyUrlsById'), state.get('currentTopologyId'), diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 41c4cc36dd..f5882cb5ef 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -7,6 +7,8 @@ import { clickNode, enterNode, leaveNode } from '../actions/app-actions'; import { getNodeColor } from '../utils/color-utils'; import MatchedText from '../components/matched-text'; import MatchedResults from '../components/matched-results'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { GRAPH_VIEW_MODE } from '../constants/naming'; import { NODE_BASE_SIZE } from '../constants/styles'; import NodeShapeStack from './node-shape-stack'; @@ -141,6 +143,11 @@ class Node extends React.Component { handleMouseClick(ev) { ev.stopPropagation(); + trackMixpanelEvent('scope.node.click', { + layout: GRAPH_VIEW_MODE, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); this.props.clickNode(this.props.id, this.props.label, this.shapeRef.getBoundingClientRect()); } @@ -159,7 +166,8 @@ function mapStateToProps(state) { return { exportingGraph: state.get('exportingGraph'), showingNetworks: state.get('showingNetworks'), - contrastMode: state.get('contrastMode') + currentTopology: state.get('currentTopology'), + contrastMode: state.get('contrastMode'), }; } diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 2d1058c9a1..7826c54ad2 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -1,11 +1,13 @@ /* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */ - import React from 'react'; import { connect } from 'react-redux'; import { List as makeList, Map as makeMap } from 'immutable'; + import NodeDetailsTable from '../components/node-details/node-details-table'; import { clickNode, sortOrderChanged } from '../actions/app-actions'; import { shownNodesSelector } from '../selectors/node-filters'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { TABLE_VIEW_MODE } from '../constants/naming'; import { canvasMarginsSelector, canvasHeightSelector } from '../selectors/canvas'; import { searchNodeMatchesSelector } from '../selectors/search'; @@ -89,6 +91,11 @@ class NodesGrid extends React.Component { if (ev.target.className === 'node-details-table-node-link') { return; } + trackMixpanelEvent('scope.node.click', { + layout: TABLE_VIEW_MODE, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); this.props.clickNode(node.id, node.label, ev.target.getBoundingClientRect()); } diff --git a/client/app/scripts/components/footer.js b/client/app/scripts/components/footer.js index 238c1597f2..5da5c3c33e 100644 --- a/client/app/scripts/components/footer.js +++ b/client/app/scripts/components/footer.js @@ -4,19 +4,39 @@ import moment from 'moment'; import Plugins from './plugins'; import { getUpdateBufferSize } from '../utils/update-buffer-utils'; -import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate, - clickResumeUpdate, toggleHelp, toggleTroubleshootingMenu, setContrastMode } from '../actions/app-actions'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { + clickDownloadGraph, + clickForceRelayout, + clickPauseUpdate, + clickResumeUpdate, + toggleHelp, + toggleTroubleshootingMenu, + setContrastMode +} from '../actions/app-actions'; + class Footer extends React.Component { constructor(props, context) { super(props, context); this.handleContrastClick = this.handleContrastClick.bind(this); + this.handleRelayoutClick = this.handleRelayoutClick.bind(this); } - handleContrastClick(e) { - e.preventDefault(); + + handleContrastClick(ev) { + ev.preventDefault(); this.props.setContrastMode(!this.props.contrastMode); } + + handleRelayoutClick(ev) { + ev.preventDefault(); + trackMixpanelEvent('scope.layout.refresh.click', { + layout: this.props.topologyViewMode, + }); + this.props.clickForceRelayout(); + } + render() { const { hostname, updatePausedAt, version, versionUpdate, contrastMode } = this.props; @@ -75,7 +95,7 @@ class Footer extends React.Component { @@ -103,9 +123,10 @@ function mapStateToProps(state) { return { hostname: state.get('hostname'), updatePausedAt: state.get('updatePausedAt'), + topologyViewMode: state.get('topologyViewMode'), version: state.get('version'), versionUpdate: state.get('versionUpdate'), - contrastMode: state.get('contrastMode') + contrastMode: state.get('contrastMode'), }; } diff --git a/client/app/scripts/components/node-details/node-details-relatives-link.js b/client/app/scripts/components/node-details/node-details-relatives-link.js index beace639d7..f7c0daa916 100644 --- a/client/app/scripts/components/node-details/node-details-relatives-link.js +++ b/client/app/scripts/components/node-details/node-details-relatives-link.js @@ -2,17 +2,23 @@ import React from 'react'; import { connect } from 'react-redux'; import { clickRelative } from '../../actions/app-actions'; +import { trackMixpanelEvent } from '../../utils/tracking-utils'; import MatchedText from '../matched-text'; + class NodeDetailsRelativesLink extends React.Component { constructor(props, context) { super(props, context); + this.handleClick = this.handleClick.bind(this); this.saveNodeRef = this.saveNodeRef.bind(this); } handleClick(ev) { ev.preventDefault(); + trackMixpanelEvent('scope.node.relative.click', { + topologyId: this.props.topologyId, + }); this.props.dispatch(clickRelative( this.props.id, this.props.topologyId, diff --git a/client/app/scripts/components/node-details/node-details-table-node-link.js b/client/app/scripts/components/node-details/node-details-table-node-link.js index e052896a3a..b5c8404dc0 100644 --- a/client/app/scripts/components/node-details/node-details-table-node-link.js +++ b/client/app/scripts/components/node-details/node-details-table-node-link.js @@ -2,16 +2,22 @@ import React from 'react'; import { connect } from 'react-redux'; import { clickRelative } from '../../actions/app-actions'; +import { trackMixpanelEvent } from '../../utils/tracking-utils'; + class NodeDetailsTableNodeLink extends React.Component { constructor(props, context) { super(props, context); + this.handleClick = this.handleClick.bind(this); this.saveNodeRef = this.saveNodeRef.bind(this); } handleClick(ev) { ev.preventDefault(); + trackMixpanelEvent('scope.node.relative.click', { + topologyId: this.props.topologyId, + }); this.props.dispatch(clickRelative( this.props.nodeId, this.props.topologyId, diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index f16c8f9237..7375b9aa75 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { Map as makeMap } from 'immutable'; import includes from 'lodash/includes'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { getCurrentTopologyOptions } from '../utils/topology-utils'; import { activeTopologyOptionsSelector } from '../selectors/topology'; import TopologyOptionAction from './topology-option-action'; @@ -12,10 +13,21 @@ class TopologyOptions extends React.Component { constructor(props, context) { super(props, context); + this.trackOptionClick = this.trackOptionClick.bind(this); this.handleOptionClick = this.handleOptionClick.bind(this); this.handleNoneClick = this.handleNoneClick.bind(this); } + trackOptionClick(optionId, nextOptions) { + trackMixpanelEvent('scope.topology.option.click', { + optionId, + value: nextOptions, + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + handleOptionClick(optionId, value, topologyId) { let nextOptions = [value]; const { activeOptions, options } = this.props; @@ -44,15 +56,18 @@ class TopologyOptions extends React.Component { nextOptions = nextOptions.filter(o => o !== 'none'); } } + this.trackOptionClick(optionId, nextOptions); this.props.changeTopologyOption(optionId, nextOptions, topologyId); } handleNoneClick(optionId, value, topologyId) { - this.props.changeTopologyOption(optionId, ['none'], topologyId); + const nextOptions = ['none']; + this.trackOptionClick(optionId, nextOptions); + this.props.changeTopologyOption(optionId, nextOptions, topologyId); } renderOption(option) { - const { activeOptions, topologyId } = this.props; + const { activeOptions, currentTopologyId } = this.props; const optionId = option.get('id'); const activeValue = activeOptions && activeOptions.has(optionId) ? activeOptions.get(optionId) @@ -68,7 +83,7 @@ class TopologyOptions extends React.Component { } @@ -102,7 +117,9 @@ class TopologyOptions extends React.Component { function mapStateToProps(state) { return { options: getCurrentTopologyOptions(state), - topologyId: state.get('currentTopologyId'), + topologyViewMode: state.get('topologyViewMode'), + currentTopology: state.get('currentTopology'), + currentTopologyId: state.get('currentTopologyId'), activeOptions: activeTopologyOptionsSelector(state) }; } diff --git a/client/app/scripts/utils/tracking-utils.js b/client/app/scripts/utils/tracking-utils.js index 3464436dcf..94f5b73253 100644 --- a/client/app/scripts/utils/tracking-utils.js +++ b/client/app/scripts/utils/tracking-utils.js @@ -6,7 +6,6 @@ const log = debug('service:tracking'); export function trackMixpanelEvent(name, props) { if (window.mixpanel && process.env.WEAVE_CLOUD) { window.mixpanel.track(name, props); - log('trackMixpanelEvent', name, props); } else { log('trackMixpanelEvent', name, props); } From 9644b97bcd206d74236078bff4ca4dd5d492934f Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 26 Apr 2017 17:15:38 +0200 Subject: [PATCH 4/6] Addressed @foot's comment. --- client/app/scripts/components/search.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 293de76539..103a9687ff 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -83,7 +83,6 @@ class Search extends React.Component { // If the search query is parsable, pin it when ENTER key is hit. if (ev.keyCode === ENTER_KEY_CODE && parseQuery(this.props.searchQuery)) { trackMixpanelEvent('scope.search.query.pin', { - query: this.props.searchQuery, layout: this.props.topologyViewMode, topologyId: this.props.currentTopology.get('id'), parentTopologyId: this.props.currentTopology.get('parentId'), @@ -99,7 +98,6 @@ class Search extends React.Component { doSearch(value) { if (value !== '') { trackMixpanelEvent('scope.search.query.change', { - query: value, layout: this.props.topologyViewMode, topologyId: this.props.currentTopology.get('id'), parentTopologyId: this.props.currentTopology.get('parentId'), From 8eaf48a534ec9234a99621f8201970fb4456bb29 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 27 Apr 2017 12:16:14 +0200 Subject: [PATCH 5/6] Added more keypress events tracking. --- client/app/scripts/actions/app-actions.js | 20 ++++++----- client/app/scripts/components/app.js | 33 +++++++++++++++++-- .../components/metric-selector-item.js | 4 +-- client/app/scripts/selectors/node-metric.js | 25 ++++++++++++++ 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 1c8a96fc16..95ab4ca93a 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -2,7 +2,6 @@ import debug from 'debug'; import ActionTypes from '../constants/action-types'; import { saveGraph } from '../utils/file-utils'; -import { modulo } from '../utils/math-utils'; import { updateRoute } from '../utils/router-utils'; import { bufferDeltaUpdate, @@ -25,7 +24,8 @@ import { storageSet } from '../utils/storage-utils'; import { loadTheme } from '../utils/contrast-utils'; import { availableMetricTypesSelector, - selectedMetricTypeSelector, + nextPinnedMetricTypeSelector, + previousPinnedMetricTypeSelector, pinnedMetricSelector, } from '../selectors/node-metric'; import { @@ -160,15 +160,17 @@ export function unpinMetric() { }; } -export function pinNextMetric(delta) { +export function pinNextMetric() { return (dispatch, getState) => { - const state = getState(); - const metricTypes = availableMetricTypesSelector(state); - const currentIndex = metricTypes.indexOf(selectedMetricTypeSelector(state)); - const nextIndex = modulo(currentIndex + delta, metricTypes.count()); - const nextMetricType = metricTypes.get(nextIndex); + const nextPinnedMetricType = nextPinnedMetricTypeSelector(getState()); + dispatch(pinMetric(nextPinnedMetricType)); + }; +} - dispatch(pinMetric(nextMetricType)); +export function pinPreviousMetric() { + return (dispatch, getState) => { + const previousPinnedMetricType = previousPinnedMetricTypeSelector(getState()); + dispatch(pinMetric(previousPinnedMetricType)); }; } diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 8564693c1e..6311912989 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -15,6 +15,7 @@ import { getApiDetails, getTopologies } from '../utils/web-api-utils'; import { focusSearch, pinNextMetric, + pinPreviousMetric, hitBackspace, hitEsc, unpinMetric, @@ -30,6 +31,7 @@ import ViewModeSelector from './view-mode-selector'; import NetworkSelector from './networks-selector'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar'; import { getRouter, getUrlState } from '../utils/router-utils'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { availableNetworksSelector } from '../selectors/node-networks'; import { activeTopologyOptionsSelector, @@ -44,10 +46,11 @@ import { const keyPressLog = debug('scope:app-key-press'); -class App extends React.Component { +class App extends React.Component { constructor(props, context) { super(props, context); + this.onKeyPress = this.onKeyPress.bind(this); this.onKeyUp = this.onKeyUp.bind(this); } @@ -97,16 +100,28 @@ class App extends React.Component { keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev); const char = String.fromCharCode(ev.charCode); if (char === '<') { - dispatch(pinNextMetric(-1)); + dispatch(pinPreviousMetric()); + this.trackEvent('scope.metric.selector.pin.previous.keypress', { + metricType: this.props.pinnedMetricType + }); } else if (char === '>') { - dispatch(pinNextMetric(1)); + dispatch(pinNextMetric()); + this.trackEvent('scope.metric.selector.pin.next.keypress', { + metricType: this.props.pinnedMetricType + }); } else if (char === 'g') { dispatch(setGraphView()); + this.trackEvent('scope.layout.selector.keypress'); } else if (char === 't') { dispatch(setTableView()); + this.trackEvent('scope.layout.selector.keypress'); } else if (char === 'r') { dispatch(setResourceView()); + this.trackEvent('scope.layout.selector.keypress'); } else if (char === 'q') { + this.trackEvent('scope.metric.selector.unpin.keypress', { + metricType: this.props.pinnedMetricType + }); dispatch(unpinMetric()); } else if (char === '/') { ev.preventDefault(); @@ -117,6 +132,15 @@ class App extends React.Component { } } + trackEvent(eventName, additionalProps = {}) { + trackMixpanelEvent(eventName, { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + ...additionalProps, + }); + } + render() { const { isTableViewMode, isGraphViewMode, isResourceViewMode, showingDetails, showingHelp, showingNetworkSelector, showingTroubleshootingMenu } = this.props; @@ -161,9 +185,11 @@ class App extends React.Component { function mapStateToProps(state) { return { activeTopologyOptions: activeTopologyOptionsSelector(state), + currentTopology: state.get('currentTopology'), isResourceViewMode: isResourceViewModeSelector(state), isTableViewMode: isTableViewModeSelector(state), isGraphViewMode: isGraphViewModeSelector(state), + pinnedMetricType: state.get('pinnedMetricType'), routeSet: state.get('routeSet'), searchFocused: state.get('searchFocused'), searchQuery: state.get('searchQuery'), @@ -172,6 +198,7 @@ function mapStateToProps(state) { showingTroubleshootingMenu: state.get('showingTroubleshootingMenu'), showingNetworkSelector: availableNetworksSelector(state).count() > 0, showingTerminal: state.get('controlPipes').size > 0, + topologyViewMode: state.get('topologyViewMode'), urlState: getUrlState(state) }; } diff --git a/client/app/scripts/components/metric-selector-item.js b/client/app/scripts/components/metric-selector-item.js index 57f8ef03e4..fda5bbce24 100644 --- a/client/app/scripts/components/metric-selector-item.js +++ b/client/app/scripts/components/metric-selector-item.js @@ -34,10 +34,10 @@ class MetricSelectorItem extends React.Component { const pinnedMetricType = this.props.pinnedMetricType; if (metricType !== pinnedMetricType) { - this.trackEvent('scope.metric.resource.pin'); + this.trackEvent('scope.metric.selector.pin.click'); this.props.pinMetric(metricType); } else { - this.trackEvent('scope.metric.resource.unpin'); + this.trackEvent('scope.metric.selector.unpin.click'); this.props.unpinMetric(); } } diff --git a/client/app/scripts/selectors/node-metric.js b/client/app/scripts/selectors/node-metric.js index 2a81ecd05f..6ac3f5d448 100644 --- a/client/app/scripts/selectors/node-metric.js +++ b/client/app/scripts/selectors/node-metric.js @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; import { createMapSelector, createListSelector } from 'reselect-map'; import { fromJS, Map as makeMap, List as makeList } from 'immutable'; +import { modulo } from '../utils/math-utils'; import { isGraphViewModeSelector, isResourceViewModeSelector } from '../selectors/topology'; import { RESOURCE_VIEW_METRICS } from '../constants/resources'; @@ -54,6 +55,30 @@ export const pinnedMetricSelector = createSelector( (availableMetrics, metricType) => availableMetrics.find(m => m.get('label') === metricType) ); +export const nextPinnedMetricTypeSelector = createSelector( + [ + availableMetricTypesSelector, + state => state.get('pinnedMetricType'), + ], + (metricTypes, pinnedMetricType) => { + const currentIndex = metricTypes.indexOf(pinnedMetricType); + const nextIndex = modulo(currentIndex + 1, metricTypes.count()); + return metricTypes.get(pinnedMetricType ? nextIndex : 0); + } +); + +export const previousPinnedMetricTypeSelector = createSelector( + [ + availableMetricTypesSelector, + state => state.get('pinnedMetricType'), + ], + (metricTypes, pinnedMetricType) => { + const currentIndex = metricTypes.indexOf(pinnedMetricType); + const previousIndex = modulo(currentIndex - 1, metricTypes.count()); + return metricTypes.get(pinnedMetricType ? previousIndex : 0); + } +); + export const selectedMetricTypeSelector = createSelector( [ state => state.get('pinnedMetricType'), From 170ea385b98eeb881ea67cb3299ec848a5de196d Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 27 Apr 2017 12:29:46 +0200 Subject: [PATCH 6/6] Disable 'r' keyboard shortcut when Resource View is disabled --- client/app/scripts/actions/app-actions.js | 25 +++++++++++-------- .../scripts/components/view-mode-selector.js | 8 +++--- client/app/scripts/selectors/topology.js | 8 ++++++ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 95ab4ca93a..3e18f40cea 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -31,6 +31,7 @@ import { import { activeTopologyOptionsSelector, isResourceViewModeSelector, + resourceViewAvailableSelector, } from '../selectors/topology'; import { GRAPH_VIEW_MODE, @@ -321,18 +322,20 @@ export function setTableView() { export function setResourceView() { return (dispatch, getState) => { - dispatch({ - type: ActionTypes.SET_VIEW_MODE, - viewMode: RESOURCE_VIEW_MODE, - }); - // Pin the first metric if none of the visible ones is pinned. - const state = getState(); - if (!pinnedMetricSelector(state)) { - const firstAvailableMetricType = availableMetricTypesSelector(state).first(); - dispatch(pinMetric(firstAvailableMetricType)); + if (resourceViewAvailableSelector(getState())) { + dispatch({ + type: ActionTypes.SET_VIEW_MODE, + viewMode: RESOURCE_VIEW_MODE, + }); + // Pin the first metric if none of the visible ones is pinned. + const state = getState(); + if (!pinnedMetricSelector(state)) { + const firstAvailableMetricType = availableMetricTypesSelector(state).first(); + dispatch(pinMetric(firstAvailableMetricType)); + } + getResourceViewNodesSnapshot(getState, dispatch); + updateRoute(getState); } - getResourceViewNodesSnapshot(getState, dispatch); - updateRoute(getState); }; } diff --git a/client/app/scripts/components/view-mode-selector.js b/client/app/scripts/components/view-mode-selector.js index e8afc26af5..ba447b3c74 100644 --- a/client/app/scripts/components/view-mode-selector.js +++ b/client/app/scripts/components/view-mode-selector.js @@ -5,9 +5,11 @@ import classNames from 'classnames'; import MetricSelector from './metric-selector'; import { trackMixpanelEvent } from '../utils/tracking-utils'; import { setGraphView, setTableView, setResourceView } from '../actions/app-actions'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { availableMetricsSelector } from '../selectors/node-metric'; -import { isResourceViewModeSelector } from '../selectors/topology'; +import { + isResourceViewModeSelector, + resourceViewAvailableSelector, +} from '../selectors/topology'; import { GRAPH_VIEW_MODE, TABLE_VIEW_MODE, @@ -68,7 +70,7 @@ class ViewModeSelector extends React.Component { function mapStateToProps(state) { return { isResourceViewMode: isResourceViewModeSelector(state), - hasResourceView: !layersTopologyIdsSelector(state).isEmpty(), + hasResourceView: resourceViewAvailableSelector(state), showingMetricsSelector: availableMetricsSelector(state).count() > 0, topologyViewMode: state.get('topologyViewMode'), currentTopology: state.get('currentTopology'), diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 30b39fea7b..90672a1d10 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect'; +import { layersTopologyIdsSelector } from './resource-view/layout'; import { RESOURCE_VIEW_MODE, GRAPH_VIEW_MODE, @@ -30,6 +31,13 @@ export const isResourceViewModeSelector = createSelector( viewMode => viewMode === RESOURCE_VIEW_MODE ); +export const resourceViewAvailableSelector = createSelector( + [ + layersTopologyIdsSelector + ], + layersTopologyIds => !layersTopologyIds.isEmpty() +); + // Checks if graph complexity is high. Used to trigger // table view on page load and decide on animations. export const graphExceedsComplexityThreshSelector = createSelector(