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>;