diff --git a/package.json b/package.json index e32471ee9..0d59f5d38 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "react-contextmenu": "2.9.1", "react-dom": "16.2.0", "react-draggable": "3.0.4", + "react-ga": "2.4.1", "react-intl": "2.4.0", "react-intl-redux": "0.6.0", "react-modal": "3.1.9", diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 1592e99c5..6dadeed1d 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -2,6 +2,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import analytics from '../../lib/analytics'; import LibraryItem from '../library-item/library-item.jsx'; import ModalComponent from '../modal/modal.jsx'; @@ -33,6 +34,11 @@ class LibraryComponent extends React.Component { handleSelect (id) { this.props.onRequestClose(); this.props.onItemSelected(this.getFilteredData()[id]); + analytics.event({ + category: 'library', + action: 'Select Item', + value: id + }); } handleMouseEnter (id) { if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]); diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index a016391e6..e546845e1 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -6,6 +6,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import VMScratchBlocks from '../lib/blocks'; import VM from 'scratch-vm'; + +import analytics from '../lib/analytics'; import Prompt from './prompt.jsx'; import BlocksComponent from '../components/blocks/blocks.jsx'; import ExtensionLibrary from './extension-library.jsx'; @@ -74,6 +76,8 @@ class Blocks extends React.Component { this.attachVM(); this.props.vm.setLocale(this.props.locale, this.props.messages); + + analytics.pageview('/editors/blocks'); } shouldComponentUpdate (nextProps, nextState) { return ( diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx index 18dd44e5e..1b4637d99 100644 --- a/src/containers/controls.jsx +++ b/src/containers/controls.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; +import analytics from '../lib/analytics'; import ControlsComponent from '../components/controls/controls.jsx'; class Controls extends React.Component { @@ -40,11 +41,19 @@ class Controls extends React.Component { this.props.vm.setTurboMode(!this.state.turbo); } else { this.props.vm.greenFlag(); + analytics.event({ + category: 'general', + action: 'Green Flag' + }); } } handleStopAllClick (e) { e.preventDefault(); this.props.vm.stopAll(); + analytics.event({ + category: 'general', + action: 'Stop Button' + }); } render () { const { diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index 6448f3e9e..38cb3aa4a 100644 --- a/src/containers/paint-editor-wrapper.jsx +++ b/src/containers/paint-editor-wrapper.jsx @@ -2,9 +2,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; import VM from 'scratch-vm'; - import PaintEditor from 'scratch-paint'; +import analytics from '../lib/analytics'; + import {connect} from 'react-redux'; class PaintEditorWrapper extends React.Component { @@ -15,6 +16,9 @@ class PaintEditorWrapper extends React.Component { 'handleUpdateSvg' ]); } + componentDidMount () { + analytics.pageview('/editors/paint'); + } handleUpdateName (name) { this.props.vm.renameCostume(this.props.selectedCostumeIndex, name); } diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 4be944a4a..e372294af 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -4,6 +4,7 @@ import React from 'react'; import {connect} from 'react-redux'; +import analytics from '../lib/analytics'; import {computeChunkedRMS} from '../lib/audio/audio-util.js'; import AudioEffects from '../lib/audio/audio-effects.js'; import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx'; @@ -40,6 +41,7 @@ class SoundEditor extends React.Component { } componentDidMount () { this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate); + analytics.pageview('/editors/sound'); } componentWillReceiveProps (newProps) { if (newProps.soundId !== this.props.soundId) { // A different sound has been selected diff --git a/src/index.jsx b/src/index.jsx index e6f350440..4eb6a686e 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Modal from 'react-modal'; +import analytics from './lib/analytics'; import AppStateHOC from './lib/app-state-hoc.jsx'; import GUI from './containers/gui.jsx'; import ProjectLoaderHOC from './lib/project-loader-hoc.jsx'; @@ -13,6 +14,9 @@ if (process.env.NODE_ENV === 'production' && typeof window === 'object') { window.onbeforeunload = () => true; } +// Register "base" page view +analytics.pageview('/'); + const App = AppStateHOC(ProjectLoaderHOC(GUI)); const appTarget = document.createElement('div'); diff --git a/src/lib/analytics.js b/src/lib/analytics.js new file mode 100644 index 000000000..a61ba2e78 --- /dev/null +++ b/src/lib/analytics.js @@ -0,0 +1,10 @@ +import GoogleAnalytics from 'react-ga'; + +GoogleAnalytics.initialize('UA-30688952-5', { + debug: (process.env.NODE_ENV !== 'production'), + titleCase: true, + sampleRate: 100, + forceSSL: true +}); + +export default GoogleAnalytics; diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index 23ad4694a..854928c7e 100644 --- a/src/lib/project-loader-hoc.jsx +++ b/src/lib/project-loader-hoc.jsx @@ -1,5 +1,6 @@ import React from 'react'; +import analytics from './analytics'; import log from './log'; import storage from './storage'; @@ -44,6 +45,15 @@ const ProjectLoaderHOC = function (WrappedComponent) { if (projectId !== this.state.projectId) { if (projectId.length < 1) projectId = 0; this.setState({projectId: projectId}); + + if (projectId !== 0) { + analytics.event({ + category: 'project', + action: 'Load Project', + value: projectId, + nonInteraction: true + }); + } } } render () { diff --git a/src/reducers/modals.js b/src/reducers/modals.js index ddc6b369c..ffbca3f8c 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -1,3 +1,5 @@ +import analytics from '../lib/analytics'; + const OPEN_MODAL = 'scratch-gui/modals/OPEN_MODAL'; const CLOSE_MODAL = 'scratch-gui/modals/CLOSE_MODAL'; @@ -50,27 +52,35 @@ const closeModal = function (modal) { }; }; const openBackdropLibrary = function () { + analytics.pageview('/libraries/backdrops'); return openModal(MODAL_BACKDROP_LIBRARY); }; const openCostumeLibrary = function () { + analytics.pageview('/libraries/costumes'); return openModal(MODAL_COSTUME_LIBRARY); }; const openExtensionLibrary = function () { + analytics.pageview('/libraries/extensions'); return openModal(MODAL_EXTENSION_LIBRARY); }; const openFeedbackForm = function () { + analytics.pageview('/modals/feedback'); return openModal(MODAL_FEEDBACK_FORM); }; const openSoundLibrary = function () { + analytics.pageview('/libraries/sounds'); return openModal(MODAL_SOUND_LIBRARY); }; const openSpriteLibrary = function () { + analytics.pageview('/libraries/sprites'); return openModal(MODAL_SPRITE_LIBRARY); }; const openSoundRecorder = function () { + analytics.pageview('/modals/microphone'); return openModal(MODAL_SOUND_RECORDER); }; const openPreviewInfo = function () { + analytics.pageview('/modals/preview'); return openModal(MODAL_PREVIEW_INFO); }; const closeBackdropLibrary = function () {