Feature(analytics): Add redux logic for analytics

Add(nav): Add event tracking to nav bar
Add(Drawer): Add event tracking to chat/map drawer
pull/7430/head
Berkeley Martinez 2016-07-21 16:35:37 -07:00
parent 63a260ae86
commit 5381b0660c
10 changed files with 192 additions and 17 deletions

View File

@ -10,7 +10,8 @@ import {
} from 'react-router-redux';
import { render } from 'redux-epic';
import { createHistory } from 'history';
import useLangRoutes from './use-lang-routes.js';
import useLangRoutes from './utils/use-lang-routes';
import sendPageAnalytics from './utils/send-page-analytics.js';
import createApp from '../common/app';
import provideStore from '../common/app/provide-store';
@ -37,6 +38,7 @@ initialState.app.csrfToken = csrfToken;
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
const history = useLangRoutes(createHistory)();
sendPageAnalytics(history, window.ga);
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
const adjustUrlOnReplay = !!window.devToolsExtension;
@ -49,7 +51,6 @@ const sagaOptions = {
history: window.history
};
createApp({
history,
syncHistoryWithStore,

View File

@ -0,0 +1,43 @@
import { Observable } from 'rx';
import { createErrorObservable } from '../../common/app/redux/actions';
import capitalize from 'lodash/capitalize';
// analytics types
// interface social {
// network: String, // facebook, twitter, etc
// action: String, // like, favorite, etc
// target: String // url like fcc.com or any other string
// }
// interface event {
// category: String,
// action: String,
// label?: String,
// value?: String
// }
//
const types = [ 'event', 'social' ];
function formatFields({ type, ...fields }) {
// make sure type is supported
if (!types.some(_type => _type === type)) {
return null;
}
return Object.keys(fields).reduce((_fields, field) => {
_fields[ type + capitalize(field) ] = fields[ field ];
return _fields;
}, { type });
}
export default function analyticsSaga(actions, getState, { window }) {
const { ga } = window;
if (typeof ga !== 'function') {
console.log('GA not found');
return Observable.empty();
}
return actions
.filter(({ meta }) => !!(meta && meta.analytics && meta.analytics.type))
.map(({ meta: { analytics } }) => formatFields(analytics))
.filter(Boolean)
// ga always returns undefined
.map(({ type, ...fields }) => ga('send', type, fields))
.catch(createErrorObservable);
}

View File

@ -7,6 +7,7 @@ import frameSaga from './frame-saga';
import codeStorageSaga from './code-storage-saga';
import gitterSaga from './gitter-saga';
import mouseTrapSaga from './mouse-trap-saga';
import analyticsSaga from './analytics-saga';
export default [
errSaga,
@ -17,5 +18,6 @@ export default [
frameSaga,
codeStorageSaga,
gitterSaga,
mouseTrapSaga
mouseTrapSaga,
analyticsSaga
];

View File

@ -0,0 +1,6 @@
export default function sendPageAnalytics(history, ga) {
history.listen(location => {
ga('set', 'page', location.pathname + location.search);
ga('send', 'pageview');
});
}

View File

@ -1,4 +1,4 @@
import { addLang, getLangFromPath } from '../common/app/utils/lang.js';
import { addLang, getLangFromPath } from '../../common/app/utils/lang.js';
function addLangToLocation(location, lang) {
if (!location) {

View File

@ -10,7 +10,8 @@ import {
updateNavHeight,
toggleMapDrawer,
toggleMainChat,
updateAppLang
updateAppLang,
trackEvent
} from './redux/actions';
import { submitChallenge } from './routes/challenges/redux/actions';
@ -26,7 +27,8 @@ const bindableActions = {
submitChallenge,
toggleMapDrawer,
toggleMainChat,
updateAppLang
updateAppLang,
trackEvent
};
const mapStateToProps = createSelector(
@ -77,7 +79,8 @@ export class FreeCodeCamp extends React.Component {
fetchUser: PropTypes.func,
shouldShowSignIn: PropTypes.bool,
params: PropTypes.object,
updateAppLang: PropTypes.func.isRequired
updateAppLang: PropTypes.func.isRequired,
trackEvent: PropTypes.func.isRequired
};
componentWillReceiveProps(nextProps) {
@ -120,7 +123,8 @@ export class FreeCodeCamp extends React.Component {
toggleMapDrawer,
toggleMainChat,
shouldShowSignIn,
params: { lang }
params: { lang },
trackEvent
} = this.props;
const navProps = {
isOnMap: router.isActive(`/${lang}/map`),
@ -130,7 +134,8 @@ export class FreeCodeCamp extends React.Component {
updateNavHeight,
toggleMapDrawer,
toggleMainChat,
shouldShowSignIn
shouldShowSignIn,
trackEvent
};
return (

View File

@ -31,7 +31,22 @@ const toggleButtonChild = (
</Col>
);
function handleNavLinkEvent(content) {
this.props.trackEvent({
category: 'Nav',
action: 'clicked',
label: `${content} link`
});
}
export default class extends React.Component {
constructor(...props) {
super(...props);
this.handleMapClickOnMap = this.handleMapClickOnMap.bind(this);
navLinks.forEach(({ content }) => {
this[`handle${content}Click`] = handleNavLinkEvent.bind(this, content);
});
}
static displayName = 'Nav';
static propTypes = {
points: PropTypes.number,
@ -42,7 +57,8 @@ export default class extends React.Component {
updateNavHeight: PropTypes.func,
toggleMapDrawer: PropTypes.func,
toggleMainChat: PropTypes.func,
shouldShowSignIn: PropTypes.bool
shouldShowSignIn: PropTypes.bool,
trackEvent: PropTypes.func.isRequired
};
componentDidMount() {
@ -50,13 +66,30 @@ export default class extends React.Component {
this.props.updateNavHeight(navBar.clientHeight);
}
handleMapClickOnMap(e) {
e.preventDefault();
this.props.trackEvent({
category: 'Nav',
action: 'clicked',
label: 'map clicked while on map'
});
}
handleNavClick() {
this.props.trackEvent({
category: 'Nav',
action: 'clicked',
label: 'map clicked while on map'
});
}
renderMapLink(isOnMap, toggleMapDrawer) {
if (isOnMap) {
return (
<li role='presentation'>
<a
href='#'
onClick={ e => e.preventDefault()}
onClick={ this.handleMapClickOnMap }
>
Map
</a>
@ -108,6 +141,7 @@ export default class extends React.Component {
<LinkContainer
eventKey={ index + 2 }
key={ content }
onClick={ this[`handle${content}Click`] }
to={ link }
>
<NavItem
@ -123,6 +157,7 @@ export default class extends React.Component {
eventKey={ index + 1 }
href={ link }
key={ content }
onClick={ this[`handle${content}Click`] }
target={ target || null }
>
{ content }

View File

@ -4,7 +4,8 @@
"target": "_blank"
},{
"content": "About",
"link": "/about"
"link": "/about",
"target": "_blank"
},{
"content": "Shop",
"link": "/shop"

View File

@ -2,6 +2,47 @@ import { Observable } from 'rx';
import { createAction } from 'redux-actions';
import types from './types';
const throwIfUndefined = () => {
throw new TypeError('Argument must not be of type `undefined`');
};
export const createEventMeta = ({
category = throwIfUndefined,
action = throwIfUndefined,
label,
value
} = throwIfUndefined) => ({
analytics: {
type: 'event',
category,
action,
label,
value
}
});
export const trackEvent = createAction(
types.analytics,
null,
createEventMeta
);
export const trackSocial = createAction(
types.analytics,
null,
(
network = throwIfUndefined,
action = throwIfUndefined,
target = throwIfUndefined
) => ({
analytics: {
type: 'event',
network,
action,
target
}
})
);
// updateTitle(title: String) => Action
export const updateTitle = createAction(types.updateTitle);
@ -79,10 +120,50 @@ export const doActionOnError = actionCreator => error => Observable.of(
// drawers
export const toggleMapDrawer = createAction(types.toggleMapDrawer);
export const toggleMainChat = createAction(types.toggleMainChat);
export const toggleHelpChat = createAction(types.toggleHelpChat);
export const openHelpChat = createAction(types.openHelpChat);
export const closeHelpChat = createAction(types.closeHelpChat);
export const toggleMapDrawer = createAction(
types.toggleMapDrawer,
null,
() => createEventMeta({
category: 'Nav',
action: 'toggled',
label: 'Map drawer toggled'
})
);
export const toggleMainChat = createAction(
types.toggleMainChat,
null,
() => createEventMeta({
category: 'Nav',
action: 'toggled',
label: 'Main chat toggled'
})
);
export const toggleHelpChat = createAction(
types.toggleHelpChat,
null,
() => createEventMeta({
category: 'Challenge',
action: 'toggled',
label: 'help chat toggled'
})
);
export const openHelpChat = createAction(
types.openHelpChat,
null,
() => createEventMeta({
category: 'Challenge',
action: 'opened',
label: 'help chat opened'
})
);
export const closeHelpChat = createAction(
types.closeHelpChat,
null,
() => createEventMeta({
category: 'Challenge',
action: 'closed',
label: 'help chat closed'
})
);
export const toggleNightMode = createAction(types.toggleNightMode);

View File

@ -1,6 +1,7 @@
import createTypes from '../utils/create-types';
export default createTypes([
'analytics',
'updateTitle',
'updateAppLang',