Feature(analytics): Add redux logic for analytics
Add(nav): Add event tracking to nav bar Add(Drawer): Add event tracking to chat/map drawerpull/7430/head
parent
63a260ae86
commit
5381b0660c
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export default function sendPageAnalytics(history, ga) {
|
||||
history.listen(location => {
|
||||
ga('set', 'page', location.pathname + location.search);
|
||||
ga('send', 'pageview');
|
||||
});
|
||||
}
|
|
@ -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) {
|
|
@ -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 (
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"target": "_blank"
|
||||
},{
|
||||
"content": "About",
|
||||
"link": "/about"
|
||||
"link": "/about",
|
||||
"target": "_blank"
|
||||
},{
|
||||
"content": "Shop",
|
||||
"link": "/shop"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import createTypes from '../utils/create-types';
|
||||
|
||||
export default createTypes([
|
||||
'analytics',
|
||||
'updateTitle',
|
||||
'updateAppLang',
|
||||
|
||||
|
|
Loading…
Reference in New Issue