Feature(analytics): Add redux logic for analytics
Add(nav): Add event tracking to nav bar Add(Drawer): Add event tracking to chat/map drawer
This commit is contained in:
@ -10,7 +10,8 @@ import {
|
|||||||
} from 'react-router-redux';
|
} from 'react-router-redux';
|
||||||
import { render } from 'redux-epic';
|
import { render } from 'redux-epic';
|
||||||
import { createHistory } from 'history';
|
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 createApp from '../common/app';
|
||||||
import provideStore from '../common/app/provide-store';
|
import provideStore from '../common/app/provide-store';
|
||||||
@ -37,6 +38,7 @@ initialState.app.csrfToken = csrfToken;
|
|||||||
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
|
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
|
||||||
|
|
||||||
const history = useLangRoutes(createHistory)();
|
const history = useLangRoutes(createHistory)();
|
||||||
|
sendPageAnalytics(history, window.ga);
|
||||||
|
|
||||||
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
||||||
const adjustUrlOnReplay = !!window.devToolsExtension;
|
const adjustUrlOnReplay = !!window.devToolsExtension;
|
||||||
@ -49,7 +51,6 @@ const sagaOptions = {
|
|||||||
history: window.history
|
history: window.history
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
createApp({
|
createApp({
|
||||||
history,
|
history,
|
||||||
syncHistoryWithStore,
|
syncHistoryWithStore,
|
||||||
|
43
client/sagas/analytics-saga.js
Normal file
43
client/sagas/analytics-saga.js
Normal 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);
|
||||||
|
}
|
@ -7,6 +7,7 @@ import frameSaga from './frame-saga';
|
|||||||
import codeStorageSaga from './code-storage-saga';
|
import codeStorageSaga from './code-storage-saga';
|
||||||
import gitterSaga from './gitter-saga';
|
import gitterSaga from './gitter-saga';
|
||||||
import mouseTrapSaga from './mouse-trap-saga';
|
import mouseTrapSaga from './mouse-trap-saga';
|
||||||
|
import analyticsSaga from './analytics-saga';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
errSaga,
|
errSaga,
|
||||||
@ -17,5 +18,6 @@ export default [
|
|||||||
frameSaga,
|
frameSaga,
|
||||||
codeStorageSaga,
|
codeStorageSaga,
|
||||||
gitterSaga,
|
gitterSaga,
|
||||||
mouseTrapSaga
|
mouseTrapSaga,
|
||||||
|
analyticsSaga
|
||||||
];
|
];
|
||||||
|
6
client/utils/send-page-analytics.js
Normal file
6
client/utils/send-page-analytics.js
Normal file
@ -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) {
|
function addLangToLocation(location, lang) {
|
||||||
if (!location) {
|
if (!location) {
|
@ -10,7 +10,8 @@ import {
|
|||||||
updateNavHeight,
|
updateNavHeight,
|
||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
updateAppLang
|
updateAppLang,
|
||||||
|
trackEvent
|
||||||
} from './redux/actions';
|
} from './redux/actions';
|
||||||
|
|
||||||
import { submitChallenge } from './routes/challenges/redux/actions';
|
import { submitChallenge } from './routes/challenges/redux/actions';
|
||||||
@ -26,7 +27,8 @@ const bindableActions = {
|
|||||||
submitChallenge,
|
submitChallenge,
|
||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
updateAppLang
|
updateAppLang,
|
||||||
|
trackEvent
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
@ -77,7 +79,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
fetchUser: PropTypes.func,
|
fetchUser: PropTypes.func,
|
||||||
shouldShowSignIn: PropTypes.bool,
|
shouldShowSignIn: PropTypes.bool,
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
updateAppLang: PropTypes.func.isRequired
|
updateAppLang: PropTypes.func.isRequired,
|
||||||
|
trackEvent: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
@ -120,7 +123,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
shouldShowSignIn,
|
shouldShowSignIn,
|
||||||
params: { lang }
|
params: { lang },
|
||||||
|
trackEvent
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const navProps = {
|
const navProps = {
|
||||||
isOnMap: router.isActive(`/${lang}/map`),
|
isOnMap: router.isActive(`/${lang}/map`),
|
||||||
@ -130,7 +134,8 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
updateNavHeight,
|
updateNavHeight,
|
||||||
toggleMapDrawer,
|
toggleMapDrawer,
|
||||||
toggleMainChat,
|
toggleMainChat,
|
||||||
shouldShowSignIn
|
shouldShowSignIn,
|
||||||
|
trackEvent
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -31,7 +31,22 @@ const toggleButtonChild = (
|
|||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handleNavLinkEvent(content) {
|
||||||
|
this.props.trackEvent({
|
||||||
|
category: 'Nav',
|
||||||
|
action: 'clicked',
|
||||||
|
label: `${content} link`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default class extends React.Component {
|
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 displayName = 'Nav';
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
@ -42,7 +57,8 @@ export default class extends React.Component {
|
|||||||
updateNavHeight: PropTypes.func,
|
updateNavHeight: PropTypes.func,
|
||||||
toggleMapDrawer: PropTypes.func,
|
toggleMapDrawer: PropTypes.func,
|
||||||
toggleMainChat: PropTypes.func,
|
toggleMainChat: PropTypes.func,
|
||||||
shouldShowSignIn: PropTypes.bool
|
shouldShowSignIn: PropTypes.bool,
|
||||||
|
trackEvent: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -50,13 +66,30 @@ export default class extends React.Component {
|
|||||||
this.props.updateNavHeight(navBar.clientHeight);
|
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) {
|
renderMapLink(isOnMap, toggleMapDrawer) {
|
||||||
if (isOnMap) {
|
if (isOnMap) {
|
||||||
return (
|
return (
|
||||||
<li role='presentation'>
|
<li role='presentation'>
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='#'
|
||||||
onClick={ e => e.preventDefault()}
|
onClick={ this.handleMapClickOnMap }
|
||||||
>
|
>
|
||||||
Map
|
Map
|
||||||
</a>
|
</a>
|
||||||
@ -108,6 +141,7 @@ export default class extends React.Component {
|
|||||||
<LinkContainer
|
<LinkContainer
|
||||||
eventKey={ index + 2 }
|
eventKey={ index + 2 }
|
||||||
key={ content }
|
key={ content }
|
||||||
|
onClick={ this[`handle${content}Click`] }
|
||||||
to={ link }
|
to={ link }
|
||||||
>
|
>
|
||||||
<NavItem
|
<NavItem
|
||||||
@ -123,6 +157,7 @@ export default class extends React.Component {
|
|||||||
eventKey={ index + 1 }
|
eventKey={ index + 1 }
|
||||||
href={ link }
|
href={ link }
|
||||||
key={ content }
|
key={ content }
|
||||||
|
onClick={ this[`handle${content}Click`] }
|
||||||
target={ target || null }
|
target={ target || null }
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
"target": "_blank"
|
"target": "_blank"
|
||||||
},{
|
},{
|
||||||
"content": "About",
|
"content": "About",
|
||||||
"link": "/about"
|
"link": "/about",
|
||||||
|
"target": "_blank"
|
||||||
},{
|
},{
|
||||||
"content": "Shop",
|
"content": "Shop",
|
||||||
"link": "/shop"
|
"link": "/shop"
|
||||||
|
@ -2,6 +2,47 @@ import { Observable } from 'rx';
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import types from './types';
|
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
|
// updateTitle(title: String) => Action
|
||||||
export const updateTitle = createAction(types.updateTitle);
|
export const updateTitle = createAction(types.updateTitle);
|
||||||
|
|
||||||
@ -79,10 +120,50 @@ export const doActionOnError = actionCreator => error => Observable.of(
|
|||||||
|
|
||||||
|
|
||||||
// drawers
|
// drawers
|
||||||
export const toggleMapDrawer = createAction(types.toggleMapDrawer);
|
export const toggleMapDrawer = createAction(
|
||||||
export const toggleMainChat = createAction(types.toggleMainChat);
|
types.toggleMapDrawer,
|
||||||
export const toggleHelpChat = createAction(types.toggleHelpChat);
|
null,
|
||||||
export const openHelpChat = createAction(types.openHelpChat);
|
() => createEventMeta({
|
||||||
export const closeHelpChat = createAction(types.closeHelpChat);
|
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);
|
export const toggleNightMode = createAction(types.toggleNightMode);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import createTypes from '../utils/create-types';
|
import createTypes from '../utils/create-types';
|
||||||
|
|
||||||
export default createTypes([
|
export default createTypes([
|
||||||
|
'analytics',
|
||||||
'updateTitle',
|
'updateTitle',
|
||||||
'updateAppLang',
|
'updateAppLang',
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user