Pass current route to router.onUpdate hook; trigger router.onUpdate from new RouterHook component; Add new event.identify hook; refactor events package, add new events-ga and events-segment packages

This commit is contained in:
SachaG 2017-12-17 17:42:06 +09:00
parent 0cbb904f2d
commit 983c08d52f
32 changed files with 391 additions and 149 deletions

View file

@ -50,10 +50,15 @@
"meteor": true,
"node": true
},
"ecmaFeatures": {
"modules": true,
"jsx": true
},
"plugins": [
"babel",
"meteor",
"react"
"react",
"prettier"
],
"settings": {
"import/resolver": "meteor"

View file

@ -12,7 +12,7 @@ import { addCallback, getActions } from 'meteor/vulcan:lib';
* @param {Object} Redux store reference instantiated on the current connected client
* @param {Object} Apollo Client reference instantiated on the current connected client
*/
function RouterClearMessages(unusedItem, store, apolloClient) {
function RouterClearMessages(unusedItem, nextRoute, store, apolloClient) {
store.dispatch(getActions().messages.clearSeen());
return unusedItem;

View file

@ -1,36 +1,68 @@
import { Components, registerComponent, registerSetting, getSetting, Strings } from 'meteor/vulcan:lib';
import {
Components,
registerComponent,
registerSetting,
getSetting,
Strings,
runCallbacks,
} from 'meteor/vulcan:lib';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, intlShape} from 'meteor/vulcan:i18n';
import { IntlProvider, intlShape } from 'meteor/vulcan:i18n';
import withCurrentUser from '../containers/withCurrentUser.js';
class App extends PureComponent {
constructor(props) {
super(props);
if (props.currentUser) {
runCallbacks('events.identify', props.currentUser);
}
}
getLocale() {
return getSetting('locale', 'en');
}
getChildContext() {
const messages = Strings[this.getLocale()] || {};
const intlProvider = new IntlProvider({locale: this.getLocale()}, messages);
const intlProvider = new IntlProvider(
{ locale: this.getLocale() },
messages
);
const { intl } = intlProvider.getChildContext();
return {
intl: intl
intl: intl,
};
}
componentWillUpdate(nextProps) {
if (nextProps.currentUser) {
runCallbacks('events.identify', nextProps.currentUser);
}
}
render() {
const currentRoute = _.last(this.props.routes);
const LayoutComponent = currentRoute.layoutName ? Components[currentRoute.layoutName] : Components.Layout;
const LayoutComponent = currentRoute.layoutName
? Components[currentRoute.layoutName]
: Components.Layout;
return (
<IntlProvider locale={this.getLocale()} messages={Strings[this.getLocale()]}>
<IntlProvider
locale={this.getLocale()}
messages={Strings[this.getLocale()]}
>
<div>
<Components.HeadTags />
<Components.RouterHook currentRoute={currentRoute} />
<LayoutComponent {...this.props} currentRoute={currentRoute}>
{ this.props.currentUserLoading ? <Components.Loading /> : (this.props.children ? this.props.children : <Components.Welcome />) }
{this.props.currentUserLoading ? (
<Components.Loading />
) : this.props.children ? (
this.props.children
) : (
<Components.Welcome />
)}
</LayoutComponent>
</div>
</IntlProvider>
@ -40,11 +72,11 @@ class App extends PureComponent {
App.propTypes = {
currentUserLoading: PropTypes.bool,
}
};
App.childContextTypes = {
intl: intlShape,
}
};
App.displayName = 'App';

View file

@ -14,9 +14,9 @@ registerSetting('faviconUrl', '/img/favicon.ico', 'Favicon absolute URL');
class HeadTags extends PureComponent {
render() {
const url = !!this.props.url ? this.props.url : Utils.getSiteUrl();
const title = !!this.props.title ? this.props.title : getSetting('title', 'My App');
const description = !!this.props.description ? this.props.description : getSetting('tagline') || getSetting('description');
const url = this.props.url || Utils.getSiteUrl();
const title = this.props.title || getSetting('title', 'My App');
const description = this.props.description || getSetting('tagline') || getSetting('description');
// default image meta: logo url, else site image defined in settings
let image = !!getSetting('siteImage') ? getSetting('siteImage'): getSetting('logoUrl');
@ -59,7 +59,7 @@ class HeadTags extends PureComponent {
{Head.meta.map((tag, index) => <meta key={index} {...tag}/>)}
{Head.link.map((tag, index) => <link key={index} {...tag}/>)}
{Head.script.map((tag, index) => <script key={index} {...tag}/>)}
{Head.script.map((tag, index) => <script key={index} {...tag}>{contents}</script>)}
</Helmet>

View file

@ -0,0 +1,24 @@
import React, { PureComponent } from 'react';
import { registerComponent, runCallbacks } from 'meteor/vulcan:lib';
import { withApollo } from 'react-apollo';
class RouterHook extends PureComponent {
constructor(props) {
super(props);
const { currentRoute, client } = props;
console.log(props)
// the first argument is an item to iterate on, needed by vulcan:lib/callbacks
// note: this item is not used in this specific callback: router.onUpdate
runCallbacks('router.onUpdate', {}, currentRoute, client.store, client);
}
componentWillReceiveProps(nextProps) {
const { currentRoute, client } = nextProps;
// the first argument is an item to iterate on, needed by vulcan:lib/callbacks
// note: this item is not used in this specific callback: router.onUpdate
runCallbacks('router.onUpdate', {}, currentRoute, client.store, client);
}
render() {
return null;
}
}
registerComponent('RouterHook', RouterHook, withApollo);

View file

@ -22,6 +22,7 @@ export { default as Datatable } from './components/Datatable.jsx';
export { default as Flash } from './components/Flash.jsx';
export { default as HelloWorld } from './components/HelloWorld.jsx';
export { default as Welcome } from './components/Welcome.jsx';
export { default as RouterHook } from './components/RouterHook.jsx';
export { default as withMessages } from "./containers/withMessages.js";
export { default as withList } from './containers/withList.js';

View file

@ -0,0 +1 @@
Vulcan events package, used internally.

View file

@ -0,0 +1,62 @@
import { getSetting } from 'meteor/vulcan:core';
import { addPageFunction, addInitFunction } from 'meteor/vulcan:events';
/*
We provide a special support for Google Analytics.
If you want to enable GA page viewing / tracking, go to
your settings file and update the 'public > googleAnalytics > apiKey'
field with your GA unique identifier (UA-xxx...).
*/
function googleAnaticsTrackPage() {
if (window && window.ga) {
window.ga('send', 'pageview', {
page: window.location.pathname,
});
}
return {};
}
// add client-side callback: log a ga request on page view
addPageFunction(googleAnaticsTrackPage);
function googleAnalyticsInit() {
// get the google analytics id from the settings
const googleAnalyticsId = getSetting('googleAnalytics.apiKey');
// the google analytics id exists & isn't the placeholder from sample_settings.json
if (googleAnalyticsId && googleAnalyticsId !== 'foo123') {
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
(i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
}),
(i[r].l = 1 * new Date());
(a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(
window,
document,
'script',
'//www.google-analytics.com/analytics.js',
'ga'
);
const cookieDomain = document.domain === 'localhost' ? 'none' : 'auto';
window.ga('create', googleAnalyticsId, cookieDomain);
// trigger first request once analytics are initialized
googleAnaticsTrackPage();
}
}
// init google analytics on the client module
addInitFunction(googleAnalyticsInit);

View file

@ -0,0 +1,2 @@
import './ga.js';
export * from '../modules/index.js';

View file

@ -0,0 +1,3 @@
import { registerSetting } from 'meteor/vulcan:core';
registerSetting('googleAnalytics.apiKey', null, 'Google Analytics ID');

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

@ -0,0 +1,19 @@
Package.describe({
name: "vulcan:events-ga",
summary: "Vulcan Google Analytics event tracking package",
version: '1.8.0',
git: "https://github.com/VulcanJS/Vulcan.git"
});
Package.onUse(function(api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0',
]);
api.mainModule("lib/server/main.js", "server");
api.mainModule('lib/client/main.js', 'client');
});

View file

@ -0,0 +1,3 @@
export * from '../modules/index';
import './segment-client.js';

View file

@ -0,0 +1,110 @@
import { getSetting, addCallback, Utils } from 'meteor/vulcan:core';
import {
addPageFunction,
addInitFunction,
addIdentifyFunction,
addTrackFunction,
} from 'meteor/vulcan:events';
/*
Track Page
*/
function segmentTrackPage(empty, route) {
const { name, path } = route;
const properties = {
url: Utils.getSiteUrl().slice(0, -1) + path,
path,
};
window.analytics.page(null, name, properties);
return {};
}
addPageFunction(segmentTrackPage);
/*
Identify User
*/
function segmentIdentify(currentUser) {
window.analytics.identify(currentUser.userId, {
email: currentUser.email,
pageUrl: currentUser.pageUrl,
});
}
addIdentifyFunction(segmentIdentify);
/*
Track Event
*/
function segmentTrack(eventName, eventProperties) {
analytics.track(eventName, eventProperties);
}
addTrackFunction(segmentTrack);
/*
Init Snippet
*/
function segmentInit() {
!(function() {
var analytics = (window.analytics = window.analytics || []);
if (!analytics.initialize)
if (analytics.invoked)
window.console &&
console.error &&
console.error('Segment snippet included twice.');
else {
analytics.invoked = !0;
analytics.methods = [
'trackSubmit',
'trackClick',
'trackLink',
'trackForm',
'pageview',
'identify',
'reset',
'group',
'track',
'ready',
'alias',
'debug',
'page',
'once',
'off',
'on',
];
analytics.factory = function(t) {
return function() {
var e = Array.prototype.slice.call(arguments);
e.unshift(t);
analytics.push(e);
return analytics;
};
};
for (var t = 0; t < analytics.methods.length; t++) {
var e = analytics.methods[t];
analytics[e] = analytics.factory(e);
}
analytics.load = function(t) {
var e = document.createElement('script');
e.type = 'text/javascript';
e.async = !0;
e.src =
('https:' === document.location.protocol ? 'https://' : 'http://') +
'cdn.segment.com/analytics.js/v1/' +
t +
'/analytics.min.js';
var n = document.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
analytics.SNIPPET_VERSION = '4.0.0';
analytics.load(getSetting('segment.clientKey'));
}
})();
}
addInitFunction(segmentInit);

View file

@ -0,0 +1,4 @@
import { registerSetting } from 'meteor/vulcan:core';
registerSetting('segment.clientKey', null, 'Segment client-side API key');
registerSetting('segment.serverKey', null, 'Segment server-side API key');

View file

@ -0,0 +1,3 @@
// export * from '../modules/index';
export * from './segment-server.js';

View file

@ -0,0 +1,5 @@
import Analytics from 'analytics-node';
import { getSetting } from 'meteor/vulcan:core';
const analytics = new Analytics(getSetting('segment.serverKey'));
export default analytics;

View file

@ -0,0 +1,19 @@
Package.describe({
name: "vulcan:events-segment",
summary: "Vulcan Segment",
version: '1.8.0',
git: "https://github.com/VulcanJS/Vulcan.git"
});
Package.onUse(function (api) {
api.versionsFrom('METEOR@1.5.2');
api.use([
'vulcan:core@1.8.0',
]);
api.mainModule('lib/server/main.js', 'server');
api.mainModule('lib/client/main.js', 'client');
});

View file

@ -1,5 +0,0 @@
import { addCallback } from 'meteor/vulcan:core';
import { sendGoogleAnalyticsRequest } from './helpers';
// add client-side callback: log a ga request on page view
addCallback('router.onUpdate', sendGoogleAnalyticsRequest);

View file

@ -1,8 +0,0 @@
import Events from './collection.js';
import { initGoogleAnalytics } from './helpers.js';
import './callbacks.js';
// init google analytics on the client module
initGoogleAnalytics();
export default Events;

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

@ -1,33 +0,0 @@
import SimpleSchema from 'simpl-schema';
const Events = new Mongo.Collection('events');
Events.schema = new SimpleSchema({
createdAt: {
type: Date
},
name: {
type: String
},
description: {
type: String,
optional: true
},
unique: {
type: Boolean,
optional: true
},
important: { // marking an event as important means it should never be erased
type: Boolean,
optional: true
},
properties: {
type: Object,
optional: true,
blackbox: true
}
});
Events.attachSchema(Events.schema);
export default Events;

View file

@ -1,60 +0,0 @@
import { getSetting, registerSetting } from 'meteor/vulcan:core';
import Events from './collection.js';
registerSetting('googleAnalyticsId', null, 'Google Analytics ID');
/*
We provide a special support for Google Analytics.
If you want to enable GA page viewing / tracking, go to
your settings file and update the 'public > googleAnalyticsId'
field with your GA unique identifier (UA-xxx...).
*/
export function sendGoogleAnalyticsRequest () {
if (window && window.ga) {
window.ga('send', 'pageview', {
'page': window.location.pathname
});
}
return {}
}
export const initGoogleAnalytics = () => {
// get the google analytics id from the settings
const googleAnalyticsId = getSetting('googleAnalyticsId');
// the google analytics id exists & isn't the placeholder from sample_settings.json
if (googleAnalyticsId && googleAnalyticsId !== 'foo123') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
const cookieDomain = document.domain === 'localhost' ? 'none' : 'auto';
window.ga('create', googleAnalyticsId, cookieDomain);
// trigger first request once analytics are initialized
sendGoogleAnalyticsRequest();
}
};
// collection based logging
Events.log = function (event) {
// if event is supposed to be unique, check if it has already been logged
if (!!event.unique && !!Events.findOne({name: event.name})) {
return;
}
event.createdAt = new Date();
Events.insert(event);
};

View file

@ -0,0 +1,16 @@
import { createCollection, getDefaultResolvers } from 'meteor/vulcan:core';
import schema from './schema.js';
const Events = createCollection({
collectionName: 'Events',
typeName: 'Event',
schema,
resolvers: getDefaultResolvers('Events'),
});
export default Events;

View file

@ -0,0 +1,29 @@
import { addCallback } from 'meteor/vulcan:core';
export const initFunctions = [];
export const trackFunctions = [];
export const addInitFunction = func => {
initFunctions.push(func);
// execute init function as soon as possible
func();
};
export const addTrackFunction = func => {
trackFunctions.push(func);
};
export const track = (eventName, eventProperties) => {
trackFunctions.forEach(f => {
f(eventName, eventProperties);
});
}
export const addIdentifyFunction = func => {
addCallback('events.identify', func);
};
export const addPageFunction = func => {
addCallback('router.onUpdate', func);
};

View file

@ -0,0 +1,2 @@
export * from './collection.js';
export * from './events';

View file

@ -0,0 +1,28 @@
const schema = {
createdAt: {
type: Date,
},
name: {
type: String,
},
description: {
type: String,
optional: true,
},
unique: {
type: Boolean,
optional: true,
},
important: {
// marking an event as important means it should never be erased
type: Boolean,
optional: true,
},
properties: {
type: Object,
optional: true,
blackbox: true,
},
};
export default schema;

View file

@ -1,18 +0,0 @@
// import { GraphQLSchema } from 'meteor/vulcan:core';
// // import Events from './collection.js';
// import { requestAnalyticsAsync } from './helpers.js';
// GraphQLSchema.addMutation('eventTrack(eventName: String, properties: JSON): JSON');
// const resolvers = {
// Mutation: {
// eventTrack: (root, { eventName, properties }, context) => {
// const user = context.currentUser || {_id: 'anonymous'};
// return properties;
// },
// },
// };
// GraphQLSchema.addResolvers(resolvers);

View file

@ -1,5 +0,0 @@
import Events from './collection.js';
import './callbacks.js';
// import './mutations.js';
export default Events;

View file

@ -0,0 +1 @@
export * from '../modules/index.js';

View file

@ -13,7 +13,7 @@ Package.onUse(function(api) {
'vulcan:core@1.8.0',
]);
api.mainModule("lib/server.js", "server");
api.mainModule("lib/client.js", "client");
api.mainModule("lib/server/main.js", "server");
api.mainModule('lib/client/main.js', 'client');
});

View file

@ -64,7 +64,7 @@ Meteor.startup(() => {
onUpdate: () => {
// the first argument is an item to iterate on, needed by vulcan:lib/callbacks
// note: this item is not used in this specific callback: router.onUpdate
runCallbacks('router.onUpdate', {}, store, apolloClient);
// runCallbacks('router.onUpdate', {}, store, apolloClient);
},
render: applyRouterMiddleware(useScroll((prevRouterProps, nextRouterProps) => {
// if the action is REPLACE, return false so that we don't jump back to top of page