diff --git a/package.json b/package.json index e04c1f2eb2b8f59ec912429294623b40c61993dc..6dc10c5514456101dba3338838597769caefb474 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,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/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx index 07b55f03e258c83fb37a9d722149183f2065f65b..e6b7d4d39f6577bf7e2bc967065a030008956848 100644 --- a/src/containers/backdrop-library.jsx +++ b/src/containers/backdrop-library.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 backdropLibraryContent from '../lib/libraries/backdrops.json'; import LibraryComponent from '../components/library/library.jsx'; @@ -27,6 +28,11 @@ class BackdropLibrary extends React.Component { this.props.onNewBackdrop(); } }); + analytics.event({ + category: 'library', + action: 'Select Backdrop', + label: item.name + }); } render () { return ( diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index a016391e636e6919432737f2b58d3acfd65b2b2c..e546845e16fd80eac75d573bf5750626a08ea024 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 18dd44e5ea6c41204f706f665c88cfbb1c093b11..1b4637d9900f3b929eb0f9a810d6dd830b5bbdfc 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/costume-library.jsx b/src/containers/costume-library.jsx index c79240f29f56ca335e3d3808f1d58f814aebf069..2e631bd95e4339344eac40cbfc3f691f4c2cd87b 100644 --- a/src/containers/costume-library.jsx +++ b/src/containers/costume-library.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 costumeLibraryContent from '../lib/libraries/costumes.json'; import LibraryComponent from '../components/library/library.jsx'; @@ -25,6 +26,11 @@ class CostumeLibrary extends React.PureComponent { this.props.vm.addCostume(item.md5, vmCostume).then(() => { this.props.onNewCostume(); }); + analytics.event({ + category: 'library', + action: 'Select Costume', + label: item.name + }); } render () { return ( diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index 0d64b564f25dc1634b919aed66625783ae7fcebf..9998fbb3e91da581176c3fbcc439ea093370652d 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -5,6 +5,7 @@ import VM from 'scratch-vm'; import extensionLibraryContent from '../lib/libraries/extensions/index'; +import analytics from '../lib/analytics'; import LibraryComponent from '../components/library/library.jsx'; import extensionIcon from '../components/sprite-selector/icon--sprite.svg'; @@ -30,6 +31,11 @@ class ExtensionLibrary extends React.PureComponent { }); } } + analytics.event({ + category: 'library', + action: 'Select Extension', + label: item.name + }); } render () { const extensionLibraryThumbnailData = extensionLibraryContent.map(extension => ({ diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index 6448f3e9e2b8901f392517728ccac4f8bdc74190..38cb3aa4a818b92ccc0ca2fa5a7946f0ade90a86 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 be67d84935b32dd63896e6e3ce13030de1426b53..b3edc48fba628e2c40981c5cb8207a1cbb484b69 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'; @@ -41,6 +42,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/containers/sound-library.jsx b/src/containers/sound-library.jsx index 7a834c2eb8e100b56279719d2920c989f9865142..928e85db8d83b57cefd4402e291daa2deb983ec1 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; import AudioEngine from 'scratch-audio'; + +import analytics from '../lib/analytics'; import LibraryComponent from '../components/library/library.jsx'; import soundIcon from '../components/asset-panel/icon--sound.svg'; @@ -58,6 +60,11 @@ class SoundLibrary extends React.PureComponent { this.props.vm.addSound(vmSound).then(() => { this.props.onNewSound(); }); + analytics.event({ + category: 'library', + action: 'Select Sound', + label: soundItem.name + }); } render () { // @todo need to use this hack to avoid library using md5 for image diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx index 6062e02a9d7c47308ed6697f4461e4ea14c088b3..00b3d07cacbe692abc51980720eff7e95ba5065f 100644 --- a/src/containers/sprite-library.jsx +++ b/src/containers/sprite-library.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 spriteLibraryContent from '../lib/libraries/sprites.json'; import LibraryComponent from '../components/library/library.jsx'; @@ -29,6 +30,11 @@ class SpriteLibrary extends React.PureComponent { } handleItemSelect (item) { this.props.vm.addSprite2(JSON.stringify(item.json)); + analytics.event({ + category: 'library', + action: 'Select Sprite', + label: item.name + }); } handleMouseEnter (item) { this.stopRotatingCostumes(); diff --git a/src/index.jsx b/src/index.jsx index e54fb895ee42c4486d51889679147c7c88980f56..114c45ce0ad6194d76270bee6e79bea378ecb9ec 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -3,6 +3,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'; @@ -14,6 +15,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 0000000000000000000000000000000000000000..7699654d1ac80ed72b8be1cc00cdf05d176300ba --- /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: (process.env.NODE_ENV === 'production') ? 100 : 0, + forceSSL: true +}); + +export default GoogleAnalytics; diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx index 23ad4694ad0fbbe34340f7eda68698ae3405d07d..854928c7eaa2720dff7150c586faf9e179d90116 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 ddc6b369c065ca1627fc454e7efe5d59138d7969..ffbca3f8c07feb4326c83e2de284eb2bf4108fc5 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 () { diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx index 790e6b85c173755adb912913718ffb4ffc652389..988aaca0f780fa61f2051eec27a0fb572fadfc55 100644 --- a/test/unit/containers/sound-editor.test.jsx +++ b/test/unit/containers/sound-editor.test.jsx @@ -7,6 +7,7 @@ import mockAudioEffects from '../../__mocks__/audio-effects.js'; import SoundEditor from '../../../src/containers/sound-editor'; import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor'; +jest.mock('react-ga'); jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer); jest.mock('../../../src/lib/audio/audio-effects', () => mockAudioEffects); diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx index 3771f37692563ebef894a719138ece5fdc4813b0..5acde098e53b8630f1a9b2dbc74177faadcb70d8 100644 --- a/test/unit/util/project-loader-hoc.test.jsx +++ b/test/unit/util/project-loader-hoc.test.jsx @@ -3,6 +3,8 @@ import ProjectLoaderHOC from '../../../src/lib/project-loader-hoc.jsx'; import storage from '../../../src/lib/storage'; import {mount} from 'enzyme'; +jest.mock('react-ga'); + describe('ProjectLoaderHOC', () => { test('when there is no project data, it renders null', () => { const Component = ({projectData}) => <div>{projectData}</div>;