From ab2ef0ccc5bb76bc177f7e496b7be059523e8052 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Sun, 24 May 2020 23:31:45 -0700 Subject: [PATCH 01/14] Mixpanel dependency and env vars. --- .example.env | 3 +++ package.json | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.example.env b/.example.env index db13ffc40..fc076d724 100644 --- a/.example.env +++ b/.example.env @@ -1,2 +1,5 @@ REACT_APP_MAPBOX_TOKEN=REDACTED DB_URL=REDACTED +MIXPANEL_ENABLED=-1 +MIXPANEL_TOKEN_PROD=REDACTED +MIXPANEL_TOKEN_DEV=REDACTED diff --git a/package.json b/package.json index 4438a22da..d98b1a7fb 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "jest": "^24.9.0", "leaflet": "^1.5.1", "leaflet.markercluster": "^1.4.1", + "mixpanel-browser": "^2.36.0", "moment": "^2.24.0", "proptypes": "^1.1.0", + "query-string": "^6.12.1", "react": "^16.8.6", "react-datepicker": "^2.12.1", "react-dom": "^16.8.6", From 34a8b53bce7570c6cfa6cb928673f0467e747bbd Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Sun, 24 May 2020 23:33:49 -0700 Subject: [PATCH 02/14] Initial Mixpanel utils with checks. --- src/utils/Mixpanel.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/utils/Mixpanel.js diff --git a/src/utils/Mixpanel.js b/src/utils/Mixpanel.js new file mode 100644 index 000000000..83b8c18b7 --- /dev/null +++ b/src/utils/Mixpanel.js @@ -0,0 +1,28 @@ +import mixpanel from 'mixpanel-browser'; + +const envCheck = process.env.NODE_ENV === 'production'; +const token = envCheck ? process.env.MIXPANEL_TOKEN_PROD : process.env.MIXPANEL_TOKEN_DEV; + +// Set MIXPANEL_ENABLED env variable to: +// 1 or greater to enable Mixpanel logging +// -1 to disable Mixpanel logging +const mixpanelEnabled = process.env.MIXPANEL_ENABLED > 0; + +if (mixpanelEnabled) { + mixpanel.init(token); +} + +const Mixpanel = { + track: (name, props) => { + if (mixpanelEnabled) { + mixpanel.track(name, props); + } + }, + time_event: name => { + if (mixpanelEnabled) { + mixpanel.time_event(name); + } + }, +}; + +export default Mixpanel; From de3330706e320d495ed60ba1106d9b14feac0cc8 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Sun, 24 May 2020 23:34:21 -0700 Subject: [PATCH 03/14] RouteChange component. --- src/components/main/util/RouteChange.jsx | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/components/main/util/RouteChange.jsx diff --git a/src/components/main/util/RouteChange.jsx b/src/components/main/util/RouteChange.jsx new file mode 100644 index 000000000..f99aa15c6 --- /dev/null +++ b/src/components/main/util/RouteChange.jsx @@ -0,0 +1,46 @@ +import { Component } from 'react'; +import { withRouter } from 'react-router-dom'; +import PropTypes from 'proptypes'; + +class RouteChange extends Component { + componentDidMount() { + this.callRouteActions(); + } + + componentDidUpdate(prevProps) { + const { location: { pathname } } = this.props; + + if (prevProps.location.pathname !== pathname) { + this.callRouteActions(); + } + } + + callRouteActions() { + const { + location, + history, + actions, + } = this.props; + + actions.forEach(action => { + action(location, history); + }); + } + + render() { + return null; + } +} + +export default withRouter(RouteChange); + +RouteChange.propTypes = { + location: PropTypes.shape({ + pathname: PropTypes.string, + }).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + replace: PropTypes.func.isRequired, + }).isRequired, + actions: PropTypes.arrayOf(PropTypes.func).isRequired, +}; From eef4cb26945e37f275935e46df6148bd1459b587 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Sun, 24 May 2020 23:37:27 -0700 Subject: [PATCH 04/14] handleReferralCode route change action. --- .../main/util/routeChangeActions.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/components/main/util/routeChangeActions.js diff --git a/src/components/main/util/routeChangeActions.js b/src/components/main/util/routeChangeActions.js new file mode 100644 index 000000000..06bbba644 --- /dev/null +++ b/src/components/main/util/routeChangeActions.js @@ -0,0 +1,22 @@ +import queryString from 'query-string'; +import Mixpanel from '@utils/Mixpanel'; + +const handleReferralCode = ( + location, + history, +) => { + const { search, pathname } = location; + const { push } = history; + const { referral } = queryString.parse(search); + + if (referral) { + Mixpanel.track('Referral Link Used', { + 'Referral code': referral, + Path: pathname, + }); + push(pathname); + } +}; + + +export default [handleReferralCode]; From 42b79e6ff10a7469d63573b35e0aa0ca8d5f08d7 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Sun, 24 May 2020 23:38:05 -0700 Subject: [PATCH 05/14] Added RouteChange to App. BrowserRouter reused. --- src/App.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index 314bcf598..f72965c36 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,10 +1,12 @@ import React, { useEffect } from 'react'; import PropTypes from 'proptypes'; import { connect } from 'react-redux'; -import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { getMetadataRequest } from '@reducers/metadata'; +import RouteChange from '@components/main/util/RouteChange'; +import actions from '@components/main/util/RouteChangeActions'; import Routes from './Routes'; import Header from './components/main/header/Header'; import Footer from './components/main/footer/Footer'; @@ -20,6 +22,7 @@ const App = ({ return ( +
From 9e9792079bea9bcf298f0020483b22576761abf0 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Sun, 24 May 2020 23:38:53 -0700 Subject: [PATCH 06/14] Updated filters with dateRange field. --- src/redux/reducers/filters.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/redux/reducers/filters.js b/src/redux/reducers/filters.js index bbc65f4d3..45cefb1c8 100644 --- a/src/redux/reducers/filters.js +++ b/src/redux/reducers/filters.js @@ -9,9 +9,9 @@ export const types = { DESELECT_ALL_REQUEST_TYPES: 'DESELECT_ALL_REQUEST_TYPES', }; -export const updateStartDate = newStartDate => ({ +export const updateStartDate = ({ dateRange, startDate }) => ({ type: types.UPDATE_START_DATE, - payload: newStartDate, + payload: { dateRange, startDate }, }); export const updateEndDate = newEndDate => ({ @@ -46,6 +46,7 @@ const allRequestTypes = value => ( ); const initialState = { + dateRange: null, startDate: null, endDate: null, councils: [], @@ -55,9 +56,11 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { case types.UPDATE_START_DATE: { + const { dateRange, startDate } = action.payload; return { ...state, - startDate: action.payload, + startDate, + dateRange, }; } case types.UPDATE_END_DATE: { From 7304e6eaef5b3812aad863d2d3541c79e560a4f5 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 00:22:54 -0700 Subject: [PATCH 07/14] Analytics saga. --- src/redux/sagas/analytics.js | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/redux/sagas/analytics.js diff --git a/src/redux/sagas/analytics.js b/src/redux/sagas/analytics.js new file mode 100644 index 000000000..0f604130b --- /dev/null +++ b/src/redux/sagas/analytics.js @@ -0,0 +1,72 @@ +import Mixpanel from '@utils/Mixpanel'; +import { select, call, takeLatest } from 'redux-saga/effects'; +// import { +// types as analyticsTypes, +// startEventTimer, +// } from '../reducers/analytics'; +import { types as dataTypes } from '../reducers/data'; + +const dataQueryEventName = 'Request Query'; +const comparisonQueryEventName = 'Comparison Query'; + + +function* timeDataQuery() { + yield call(Mixpanel.time_event, dataQueryEventName); +} + +/* ////////////////// FILTERS //////////////// */ + +const getState = (state, slice) => state[slice]; + +function countDaysBetweenDates(startDate, endDate) { + const date1 = new Date(startDate); + const date2 = new Date(endDate); + const diffTime = Math.abs(date2 - date1); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; +} + +function* getQueryFilters() { + const { + dateRange, + startDate, + endDate, + councils, + requestTypes, + } = yield select(getState, 'filters'); + + const filters = { + 'Time Frame': dateRange, + 'Time Frame Custom Start': false, + 'Time Frame Custom End': false, + 'Time Frame Custom Span': false, + Areas: undefined, + 'Areas Fully Selected': undefined, + 'NC Count': councils.length, + 'Request Types': requestTypes, + }; + + if (dateRange === 'CUSTOM_DATE_RANGE') { + const customStartDate = new Date(startDate).toIsoString(); + const customEndDate = new Date(endDate).toIsoString(); + filters['Time Frame Custom Start'] = customStartDate; + filters['Time Frame Custom End'] = customEndDate; + filters['Time Frame Custom Span'] = countDaysBetweenDates(customStartDate, customEndDate); + } + + console.log(filters); + return filters; +} + +/* ////////////////// SAGAS //////////////// */ + +function* logQueryFilters() { + const filters = yield getQueryFilters(); + // console.log(filters); + yield call(Mixpanel.track, dataQueryEventName, filters); +} + +export default function* rootSaga() { + yield takeLatest(dataTypes.GET_DATA_REQUEST, timeDataQuery); + yield takeLatest(dataTypes.GET_HEATMAP_SUCCESS, logQueryFilters); +} From 1d922c223eaccd234e478cf3854fa32bd8a3dd16 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 00:25:08 -0700 Subject: [PATCH 08/14] Added analytics to rootSaga. --- src/redux/rootSaga.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/redux/rootSaga.js b/src/redux/rootSaga.js index 17bb6b993..8272c8423 100644 --- a/src/redux/rootSaga.js +++ b/src/redux/rootSaga.js @@ -3,6 +3,7 @@ import { all } from 'redux-saga/effects'; import metadata from './sagas/metadata'; import data from './sagas/data'; import comparisonData from './sagas/comparisonData'; +import analytics from './sagas/analytics'; export default function* rootSaga() { @@ -10,5 +11,6 @@ export default function* rootSaga() { metadata(), data(), comparisonData(), + analytics(), ]); } From 0986b0dbf51122066f4c5c49d1ca9328115f0ae8 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 23:01:25 -0700 Subject: [PATCH 09/14] Analytics action creators and sagas. --- src/redux/reducers/analytics.js | 13 ++ src/redux/sagas/analytics.js | 271 ++++++++++++++++++++++++++++---- 2 files changed, 250 insertions(+), 34 deletions(-) create mode 100644 src/redux/reducers/analytics.js diff --git a/src/redux/reducers/analytics.js b/src/redux/reducers/analytics.js new file mode 100644 index 000000000..34258412b --- /dev/null +++ b/src/redux/reducers/analytics.js @@ -0,0 +1,13 @@ +export const types = { + TRACK_MAP_EXPORT: 'TRACK_MAP_EXPORT', + TRACK_CHART_EXPORT: 'TRACK_CHART_EXPORT', +}; + +export const trackMapExport = () => ({ + type: types.TRACK_MAP_EXPORT, +}); + +export const trackChartExport = ({ pageArea, fileType, path }) => ({ + type: types.TRACK_CHART_EXPORT, + payload: { pageArea, fileType, path }, +}); diff --git a/src/redux/sagas/analytics.js b/src/redux/sagas/analytics.js index 0f604130b..85df95c0f 100644 --- a/src/redux/sagas/analytics.js +++ b/src/redux/sagas/analytics.js @@ -1,72 +1,275 @@ import Mixpanel from '@utils/Mixpanel'; import { select, call, takeLatest } from 'redux-saga/effects'; -// import { -// types as analyticsTypes, -// startEventTimer, -// } from '../reducers/analytics'; -import { types as dataTypes } from '../reducers/data'; - -const dataQueryEventName = 'Request Query'; -const comparisonQueryEventName = 'Comparison Query'; +import { types as dataTypes } from '../reducers/data'; +import { types as analyticsTypes } from '../reducers/analytics'; +import { types as comparisonTypes } from '../reducers/comparisonData'; +import { COUNCILS } from '../../components/common/CONSTANTS'; -function* timeDataQuery() { - yield call(Mixpanel.time_event, dataQueryEventName); -} +const events = { + dataQuery: 'Request Query', + comparisonQuery: 'Comparison Query', + exportRequest: 'Export Request', + exportComparison: 'Export Comparison', +}; /* ////////////////// FILTERS //////////////// */ const getState = (state, slice) => state[slice]; -function countDaysBetweenDates(startDate, endDate) { +const regionCounts = COUNCILS.reduce((acc, curr) => { + const { region } = curr; + if (!acc[region]) { + acc[region] = 1; + } else { + acc[region] += 1; + } + return acc; +}, {}); + +const countDaysBetweenDates = (startDate, endDate) => { const date1 = new Date(startDate); const date2 = new Date(endDate); const diffTime = Math.abs(date2 - date1); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; -} - -function* getQueryFilters() { - const { - dateRange, - startDate, - endDate, - councils, - requestTypes, - } = yield select(getState, 'filters'); +}; +const dateFilters = (dateRange, startDate, endDate) => { const filters = { 'Time Frame': dateRange, 'Time Frame Custom Start': false, 'Time Frame Custom End': false, 'Time Frame Custom Span': false, - Areas: undefined, - 'Areas Fully Selected': undefined, - 'NC Count': councils.length, - 'Request Types': requestTypes, }; if (dateRange === 'CUSTOM_DATE_RANGE') { - const customStartDate = new Date(startDate).toIsoString(); - const customEndDate = new Date(endDate).toIsoString(); + const customStartDate = new Date(startDate).toISOString(); + const customEndDate = new Date(endDate).toISOString(); filters['Time Frame Custom Start'] = customStartDate; filters['Time Frame Custom End'] = customEndDate; filters['Time Frame Custom Span'] = countDaysBetweenDates(customStartDate, customEndDate); } - console.log(filters); return filters; +}; + +const councilAreaFilters = councils => { + const filters = { + Areas: [], + 'Areas Fully Selected': [], + }; + const regions = { ...regionCounts }; + + councils.forEach(name => { + const region = COUNCILS.find(nc => nc.name === name)?.region; + regions[region] -= 1; + + if (!filters.Areas.includes(region)) { + filters.Areas.push(region); + } else if (!regions[region]) { + filters['Areas Fully Selected'].push(region); + } + }); + + return filters; +}; + +const comparisonSetFilters = (set1, set2) => { + const filters = { + 'Set1 District Type': set1.district, + 'Set1 Councils': set1.list, + 'Set1 Areas': [], + 'Set1 Areas Fully Selected': [], + 'Set2 District Type': set2.district, + 'Set2 Councils': set2.list, + 'Set2 Areas': [], + 'Set2 Areas Fully Selected': [], + }; + + if (set1.district === 'nc') { + const regions = { ...regionCounts }; + + set1.list.forEach(name => { + const region = COUNCILS.find(nc => nc.name === name)?.region; + regions[region] -= 1; + + if (!filters['Set1 Areas'].includes(region)) { + filters['Set1 Areas'].push(region); + } else if (!regions[region]) { + filters['Set1 Areas Fully Selected'].push(region); + } + }); + } + + if (set2.district === 'nc') { + const regions = { ...regionCounts }; + + set2.list.forEach(name => { + const region = COUNCILS.find(nc => nc.name === name)?.region; + regions[region] -= 1; + + if (!filters['Set2 Areas'].includes(region)) { + filters['Set2 Areas'].push(region); + } else if (!regions[region]) { + filters['Set2 Areas Fully Selected'].push(region); + } + }); + } + return filters; +}; + +const requestTypeFilters = requestTypes => ( + { 'Request Types': Object.keys(requestTypes).filter(req => requestTypes[req]) } +); + +/** + * Converts 311Data filters into Mixpanel event properties object. + * { + * 'Time Frame': LAST_6_MONTHS, + * 'Time Frame Custom Start': false, + * 'Time Frame Custom End': false, + * 'Time Frame Custom Span': false, + * Councils: ['Atwater Village', 'Echo Park', ...], + * 'NC Count': councils.length, + * Areas: ['East', 'Central 2', ...], + * 'Areas Fully Selected': ['East'], + * 'Request Types': ['Bulky Items', 'Single Streetlight', ...] + * } + */ +function* getAllDataFilters() { + const { + dateRange, + startDate, + endDate, + councils, + requestTypes, + } = yield select(getState, 'filters'); + const dateFields = dateFilters(dateRange, startDate, endDate); + const areaFields = councilAreaFilters(councils); + const typeFields = requestTypeFilters(requestTypes); + + return { + Councils: councils, + 'NC Count': councils.length, + ...dateFields, + ...areaFields, + ...typeFields, + }; +} + +/** + * Converts comparison filters into Mixpanel event properties object. + * { + * 'Time Frame': LAST_6_MONTHS, + * 'Time Frame Custom Start': false, + * 'Time Frame Custom End': false, + * 'Time Frame Custom Span': false, + * 'Chart Type': 'frequency', + * 'Set1 District Type': 'nc', + * 'Set1 Councils': ['Atwater Village', 'Echo Park', ...], + * 'Set1 Areas': ['East', 'Central 2', ...], + * 'Set1 Areas Fully Selected': ['East'], + * 'Set2 District Type': 'cc', + * 'Set2 Councils': ['Council District 1', 'Council District 2', ...], + * 'Set2 Areas': [], + * 'Set2 Areas Fully Selected': [], + * 'Request Types': ['Bulky Items', 'Single Streetlight', ...] + * } + */ +function* getAllComparisonFilters() { + const { + dateRange, + startDate, + endDate, + comparison: { + chart, + set1, + set2, + }, + requestTypes, + } = yield select(getState, 'comparisonFilters'); + const dateFields = dateFilters(dateRange, startDate, endDate); + const setFields = comparisonSetFilters(set1, set2); + const typeFields = requestTypeFilters(requestTypes); + + return { + ...dateFields, + 'Chart Type': chart, + ...setFields, + ...typeFields, + }; } /* ////////////////// SAGAS //////////////// */ -function* logQueryFilters() { - const filters = yield getQueryFilters(); - // console.log(filters); - yield call(Mixpanel.track, dataQueryEventName, filters); +/* // DATA/COMPARISON QUERIES // */ + +function* timeDataQuery() { + yield call(Mixpanel.time_event, events.dataQuery); +} + +function* logDataQuery() { + const filters = yield getAllDataFilters(); + yield call(Mixpanel.track, events.dataQuery, filters); +} + +function* timeComparisonQuery() { + yield call(Mixpanel.time_event, events.comparisonQuery); +} + +function* logComparisonQuery() { + const eventProps = yield getAllComparisonFilters(); + yield call(Mixpanel.track, events.comparisonQuery, eventProps); +} + +/* // EXPORTS // */ + +function* logMapExport() { + const filters = yield getAllDataFilters(); + const eventProps = { + 'Page Area': 'Map', + Filetype: 'PNG', + ...filters, + }; + yield call(Mixpanel.track, events.exportRequest, eventProps); +} + +function* logChartExport(action) { + const { + pageArea, + fileType, + path, + } = action.payload; + let eventName; + const eventProps = { + 'Page Area': pageArea, + Filetype: fileType, + path, + }; + + if (path === '/comparison') { + eventName = events.exportComparison; + const filters = yield getAllComparisonFilters(); + Object.assign(eventProps, { + ...filters, + }); + } else { + eventName = events.exportRequest; + const filters = yield getAllDataFilters(); + Object.assign(eventProps, { + ...filters, + }); + } + + yield call(Mixpanel.track, eventName, eventProps); } export default function* rootSaga() { yield takeLatest(dataTypes.GET_DATA_REQUEST, timeDataQuery); - yield takeLatest(dataTypes.GET_HEATMAP_SUCCESS, logQueryFilters); + yield takeLatest(dataTypes.GET_HEATMAP_SUCCESS, logDataQuery); + yield takeLatest(comparisonTypes.GET_COMPARISON_DATA_REQUEST, timeComparisonQuery); + yield takeLatest(comparisonTypes.GET_COMPARISON_DATA_SUCCESS, logComparisonQuery); + yield takeLatest(analyticsTypes.TRACK_MAP_EXPORT, logMapExport); + yield takeLatest(analyticsTypes.TRACK_CHART_EXPORT, logChartExport); } From d590cff3786a46afe862459d35524a2ef3c8f784 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 23:03:39 -0700 Subject: [PATCH 10/14] Updated comparisonFilters reducers. --- src/redux/reducers/comparisonFilters.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/redux/reducers/comparisonFilters.js b/src/redux/reducers/comparisonFilters.js index ce079d6ba..3549878d1 100644 --- a/src/redux/reducers/comparisonFilters.js +++ b/src/redux/reducers/comparisonFilters.js @@ -12,9 +12,9 @@ export const types = { DESELECT_ALL_COMPARISON_REQUEST_TYPES: 'DESELECT_ALL_COMPARISON_REQUEST_TYPES', }; -export const updateComparisonStartDate = newStartDate => ({ +export const updateComparisonStartDate = ({ dateRange, startDate }) => ({ type: types.UPDATE_COMPARISON_START_DATE, - payload: newStartDate, + payload: { dateRange, startDate }, }); export const updateComparisonEndDate = newEndDate => ({ @@ -70,6 +70,7 @@ const allRequestTypes = value => ( ); const initialState = { + dateRange: null, startDate: null, endDate: null, comparison: { @@ -89,9 +90,11 @@ const initialState = { export default (state = initialState, action) => { switch (action.type) { case types.UPDATE_COMPARISON_START_DATE: { + const { dateRange, startDate } = action.payload; return { ...state, - startDate: action.payload, + startDate, + dateRange, }; } case types.UPDATE_COMPARISON_END_DATE: { From cd120199a1f219cb2ce8c1e49acdd60ded8347dc Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 23:05:17 -0700 Subject: [PATCH 11/14] Added logging actions to components. --- src/components/PinMap/PinMap.jsx | 5 +++ src/components/export/ChartExportSelect.jsx | 31 +++++++++++++++++-- src/components/export/VisExportSelect.jsx | 24 ++++++++++++-- .../menu/DateSelector/DateRangePicker.jsx | 2 +- .../main/menu/DateSelector/DateSelector.jsx | 2 +- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/components/PinMap/PinMap.jsx b/src/components/PinMap/PinMap.jsx index 94b024e67..25185e630 100644 --- a/src/components/PinMap/PinMap.jsx +++ b/src/components/PinMap/PinMap.jsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { getPinInfoRequest } from '@reducers/data'; import { updateMapPosition } from '@reducers/ui'; +import { trackMapExport } from '@reducers/analytics'; import PinPopup from '@components/PinMap/PinPopup'; import CustomMarker from '@components/PinMap/CustomMarker'; import ClusterMarker from '@components/PinMap/ClusterMarker'; @@ -330,9 +331,11 @@ class PinMap extends Component { id="map-export" label="Export" handleClick={() => { + const { exportMap } = this.props; const selector = '.leaflet-control-easyPrint .CurrentSize'; const link = document.body.querySelector(selector); if (link) link.click(); + exportMap(); }} />
@@ -364,6 +367,7 @@ class PinMap extends Component { const mapDispatchToProps = dispatch => ({ getPinInfo: srnumber => dispatch(getPinInfoRequest(srnumber)), updatePosition: position => dispatch(updateMapPosition(position)), + exportMap: () => dispatch(trackMapExport()), }); const mapStateToProps = state => ({ @@ -378,6 +382,7 @@ PinMap.propTypes = { heatmap: PropTypes.arrayOf(PropTypes.array), getPinInfo: PropTypes.func.isRequired, updatePosition: PropTypes.func.isRequired, + exportMap: PropTypes.func.isRequired, }; PinMap.defaultProps = { diff --git a/src/components/export/ChartExportSelect.jsx b/src/components/export/ChartExportSelect.jsx index cbe2e1f72..64a212b2f 100644 --- a/src/components/export/ChartExportSelect.jsx +++ b/src/components/export/ChartExportSelect.jsx @@ -1,5 +1,8 @@ import React, { useState } from 'react'; +import { useLocation } from 'react-router-dom'; import PropTypes from 'proptypes'; +import { connect } from 'react-redux'; +import { trackChartExport } from '@reducers/analytics'; import LoaderButton from '@components/common/LoaderButton'; import SelectItem from './SelectItem'; import { getImage, getCsv, getSinglePagePdf } from './BlobFactory'; @@ -9,9 +12,11 @@ const ChartExportSelect = ({ pdfTemplateName, exportData, filename, + trackExport, }) => { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); + const { pathname } = useLocation(); return ( { setOpen(false); setLoading(true); + trackExport({ + pageArea: `${filename} Chart`, + fileType: 'PNG', + path: pathname, + }); }} onComplete={() => setLoading(false)} /> @@ -50,6 +60,11 @@ const ChartExportSelect = ({ onClick={() => { setOpen(false); setLoading(true); + trackExport({ + pageArea: `${filename} Chart`, + fileType: 'PDF', + path: pathname, + }); }} onComplete={() => setLoading(false)} /> @@ -62,7 +77,14 @@ const ChartExportSelect = ({ label="CSV" filename={`${filename}.csv`} getData={() => getCsv(exportData())} - onClick={() => setOpen(false)} + onClick={() => { + setOpen(false); + trackExport({ + pageArea: `${filename} Chart`, + fileType: 'CSV', + path: pathname, + }); + }} /> {/*
Excel
*/} @@ -72,13 +94,18 @@ const ChartExportSelect = ({ ); }; -export default ChartExportSelect; +const mapDispatchToProps = dispatch => ({ + trackExport: properties => dispatch(trackChartExport(properties)), +}); + +export default connect(null, mapDispatchToProps)(ChartExportSelect); ChartExportSelect.propTypes = { componentName: PropTypes.string, pdfTemplateName: PropTypes.string, exportData: PropTypes.func, filename: PropTypes.string, + trackExport: PropTypes.func.isRequired, }; ChartExportSelect.defaultProps = { diff --git a/src/components/export/VisExportSelect.jsx b/src/components/export/VisExportSelect.jsx index 777c02a8c..725cecee5 100644 --- a/src/components/export/VisExportSelect.jsx +++ b/src/components/export/VisExportSelect.jsx @@ -1,11 +1,18 @@ import React, { useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import PropTypes from 'proptypes'; +import { connect } from 'react-redux'; +import { trackChartExport } from '@reducers/analytics'; import LoaderButton from '@components/common/LoaderButton'; import SelectItem from './SelectItem'; import { getMultiPagePdf } from './BlobFactory'; -const VisExportSelect = () => { +const VisExportSelect = ({ + trackExport, +}) => { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); + const location = useLocation(); return ( { onClick={() => { setOpen(false); setLoading(true); + trackExport({ + pageArea: 'Data Visualizations', + fileType: 'PDF', + path: location.pathname, + }); }} onComplete={() => setLoading(false)} /> @@ -42,4 +54,12 @@ const VisExportSelect = () => { ); }; -export default VisExportSelect; +const mapDispatchToProps = dispatch => ({ + trackExport: properties => dispatch(trackChartExport(properties)), +}); + +VisExportSelect.propTypes = { + trackExport: PropTypes.func.isRequired, +}; + +export default connect(null, mapDispatchToProps)(VisExportSelect); diff --git a/src/components/main/menu/DateSelector/DateRangePicker.jsx b/src/components/main/menu/DateSelector/DateRangePicker.jsx index d6bd6b168..99ff3c96c 100644 --- a/src/components/main/menu/DateSelector/DateRangePicker.jsx +++ b/src/components/main/menu/DateSelector/DateRangePicker.jsx @@ -134,7 +134,7 @@ const DateRangePicker = ({ const formatDate = date => moment(date).format('MM/DD/YYYY'); const dispatchStart = comparison ? updateComparisonStart : updateStart; const dispatchEnd = comparison ? updateComparisonEnd : updateEnd; - dispatchStart(formatDate(startDate)); + dispatchStart({ dateRange: 'CUSTOM_DATE_RANGE', startDate: formatDate(startDate) }); dispatchEnd(formatDate(endDate)); handleClick(); } else if (!startDate && !endDate) { diff --git a/src/components/main/menu/DateSelector/DateSelector.jsx b/src/components/main/menu/DateSelector/DateSelector.jsx index 353fc4139..c7895b2a7 100644 --- a/src/components/main/menu/DateSelector/DateSelector.jsx +++ b/src/components/main/menu/DateSelector/DateSelector.jsx @@ -75,7 +75,7 @@ const DateSelector = ({ if (dateOption !== 'CUSTOM_DATE_RANGE') { const { newStartDate, newEndDate } = getDates(dateOption); - dispatchStart(newStartDate); + dispatchStart({ dateRange: dateOption, startDate: newStartDate }); dispatchEnd(newEndDate); } else { setModalOpen(true); From 885092b89bb6f33ffb181318db914542aaa58289 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 23:08:47 -0700 Subject: [PATCH 12/14] Added google analytics script to index. --- public/index.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 13049e129..450d9bfc0 100644 --- a/public/index.html +++ b/public/index.html @@ -3,12 +3,16 @@ + + + + - + 311 Data From bf139cdd78c845905b27373f329dd73535d27199 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Mon, 25 May 2020 23:23:48 -0700 Subject: [PATCH 13/14] Added logAboutPageVisit to RouteChange actions. --- src/components/main/util/routeChangeActions.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/main/util/routeChangeActions.js b/src/components/main/util/routeChangeActions.js index 06bbba644..5aed0c33c 100644 --- a/src/components/main/util/routeChangeActions.js +++ b/src/components/main/util/routeChangeActions.js @@ -18,5 +18,15 @@ const handleReferralCode = ( } }; +const logAboutPageVisit = location => { + const { pathname } = location; + if (pathname === '/about') { + Mixpanel.track('About 311'); + } +}; + -export default [handleReferralCode]; +export default [ + handleReferralCode, + logAboutPageVisit, +]; From b28d494cfbd0c7457e869280c6abfa6cc37b67c4 Mon Sep 17 00:00:00 2001 From: Adam Kendis Date: Tue, 26 May 2020 00:37:47 -0700 Subject: [PATCH 14/14] Fix linting error. --- src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index f72965c36..0b5a37b18 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -6,7 +6,7 @@ import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { getMetadataRequest } from '@reducers/metadata'; import RouteChange from '@components/main/util/RouteChange'; -import actions from '@components/main/util/RouteChangeActions'; +import actions from '@components/main/util/routeChangeActions'; import Routes from './Routes'; import Header from './components/main/header/Header'; import Footer from './components/main/footer/Footer';