diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 97741a147..dfcc7d8dd 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -97,6 +97,7 @@ const GUIComponent = props => { onActivateTab, onClickLogo, onExtensionButtonClick, + onProjectTelemetryEvent, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onRequestCloseTelemetryModal, @@ -217,6 +218,7 @@ const GUIComponent = props => { onCloseAccountNav={onCloseAccountNav} onLogOut={onLogOut} onOpenRegistration={onOpenRegistration} + onProjectTelemetryEvent={onProjectTelemetryEvent} onSeeCommunity={onSeeCommunity} onShare={onShare} onToggleLoginOpen={onToggleLoginOpen} diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index f985881c1..972809d2d 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -6,6 +6,8 @@ import bindAll from 'lodash.bindall'; import bowser from 'bowser'; import React from 'react'; +import VM from 'scratch-vm'; + import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; import CommunityButton from './community-button.jsx'; @@ -55,6 +57,8 @@ import { loginMenuOpen } from '../../reducers/menus'; +import collectMetadata from '../../lib/collect-metadata'; + import styles from './menu-bar.css'; import helpIcon from '../../lib/assets/icon--tutorials.svg'; @@ -146,10 +150,10 @@ class MenuBar extends React.Component { 'handleClickSaveAsCopy', 'handleClickSeeCommunity', 'handleClickShare', - 'handleCloseFileMenuAndThen', 'handleKeyPress', 'handleLanguageMouseUp', 'handleRestoreOption', + 'handleSaveToComputer', 'restoreOptionMessage' ]); } @@ -216,12 +220,6 @@ class MenuBar extends React.Component { this.props.onRequestCloseEdit(); }; } - handleCloseFileMenuAndThen (fn) { - return () => { - this.props.onRequestCloseFile(); - fn(); - }; - } handleKeyPress (event) { const modifier = bowser.mac ? event.metaKey : event.ctrlKey; if (modifier && event.key === 's') { @@ -229,6 +227,16 @@ class MenuBar extends React.Component { event.preventDefault(); } } + handleSaveToComputer (downloadProjectCallback) { + return () => { + this.props.onRequestCloseFile(); + downloadProjectCallback(); + if (this.props.onProjectTelemetryEvent) { + const metadata = collectMetadata(this.props.vm, this.props.projectTitle, this.props.locale); + this.props.onProjectTelemetryEvent('projectDidSave', metadata); + } + }; + } handleLanguageMouseUp (e) { if (!this.props.languageMenuOpen) { this.props.onClickLanguage(e); @@ -402,10 +410,10 @@ class MenuBar extends React.Component { )} - {(className, downloadProject) => ( + {(className, downloadProjectCallback) => ( { isUpdating: getIsUpdating(loadingState), isShowingProject: getIsShowingProject(loadingState), languageMenuOpen: languageMenuOpen(state), + locale: state.locales.locale, loginMenuOpen: loginMenuOpen(state), projectChanged: state.scratchGui.projectChanged, projectTitle: state.scratchGui.projectTitle, sessionExists: state.session && typeof state.session.session !== 'undefined', - username: user ? user.username : null + username: user ? user.username : null, + vm: state.scratchGui.vm }; }; diff --git a/src/lib/collect-metadata.js b/src/lib/collect-metadata.js new file mode 100644 index 000000000..d71b4448b --- /dev/null +++ b/src/lib/collect-metadata.js @@ -0,0 +1,39 @@ +/** + * Report a telemetry event. + * @param {string} event - one of `projectWasCreated`, `projectDidLoad`, `projectDidSave`, `projectWasUploaded` + */ +// TODO make a telemetry HOC and move this stuff there +const collectMetadata = function (vm, projectName = '', locale = '') { + // TODO move most or all of this into a collectMetadata() method on the VM/Runtime + const metadata = { + projectName: projectName, + language: locale, + spriteCount: 0, + blocksCount: 0, + costumesCount: 0, + listsCount: 0, + scriptCount: 0, + soundsCount: 0, + variablesCount: 0 + }; + + for (const target of vm.runtime.targets) { + ++metadata.spriteCount; + metadata.blocksCount += Object.keys(target.sprite.blocks._blocks).length; + metadata.costumesCount += target.sprite.costumes_.length; + metadata.scriptCount += target.sprite.blocks._scripts.length; + metadata.soundsCount += target.sprite.sounds.length; + for (const variableName in target.variables) { + const variable = target.variables[variableName]; + if (variable.type === 'list') { + ++metadata.listsCount; + } else { + ++metadata.variablesCount; + } + } + } + + return metadata; +}; + +export default collectMetadata; diff --git a/src/lib/project-saver-hoc.jsx b/src/lib/project-saver-hoc.jsx index ab51138a4..2cee625ec 100644 --- a/src/lib/project-saver-hoc.jsx +++ b/src/lib/project-saver-hoc.jsx @@ -6,6 +6,7 @@ import {connect} from 'react-redux'; import VM from 'scratch-vm'; import xhr from 'xhr'; +import collectMetadata from '../lib/collect-metadata'; import log from '../lib/log'; import storage from '../lib/storage'; import dataURItoBlob from '../lib/data-uri-to-blob'; @@ -25,6 +26,7 @@ import { getIsAnyCreatingNewState, getIsCreatingCopy, getIsCreatingNew, + getIsLoading, getIsManualUpdating, getIsRemixing, getIsShowingWithId, @@ -61,6 +63,13 @@ const ProjectSaverHOC = function (WrappedComponent) { this.props.onSetProjectThumbnailer(this.getProjectThumbnail); } componentDidUpdate (prevProps) { + if (!this.props.isAnyCreatingNewState && prevProps.isAnyCreatingNewState) { + this.reportTelemetryEvent('projectWasCreated'); + } + if (!this.props.isLoading && prevProps.isLoading) { + this.reportTelemetryEvent('projectDidLoad'); + } + if (this.props.projectChanged && !prevProps.projectChanged) { this.scheduleAutoSave(); } @@ -305,6 +314,18 @@ const ProjectSaverHOC = function (WrappedComponent) { this.props.vm.renderer.draw(); } + /** + * Report a telemetry event. + * @param {string} event - one of `projectWasCreated`, `projectDidLoad`, `projectDidSave`, `projectWasUploaded` + */ + // TODO make a telemetry HOC and move this stuff there + reportTelemetryEvent (event) { + if (this.props.onProjectTelemetryEvent) { + const metadata = collectMetadata(this.props.vm, this.props.reduxProjectTitle, this.props.locale); + this.props.onProjectTelemetryEvent(event, metadata); + } + } + render () { const { /* eslint-disable no-unused-vars */ @@ -314,6 +335,7 @@ const ProjectSaverHOC = function (WrappedComponent) { isCreatingNew, projectChanged, isAnyCreatingNewState, + isLoading, isManualUpdating, isRemixing, isShowingSaveable, @@ -359,6 +381,7 @@ const ProjectSaverHOC = function (WrappedComponent) { isAnyCreatingNewState: PropTypes.bool, isCreatingCopy: PropTypes.bool, isCreatingNew: PropTypes.bool, + isLoading: PropTypes.bool, isManualUpdating: PropTypes.bool, isRemixing: PropTypes.bool, isShared: PropTypes.bool, @@ -371,6 +394,7 @@ const ProjectSaverHOC = function (WrappedComponent) { onCreateProject: PropTypes.func, onCreatedProject: PropTypes.func, onProjectError: PropTypes.func, + onProjectTelemetryEvent: PropTypes.func, onRemixing: PropTypes.func, onShowAlert: PropTypes.func, onShowCopySuccessAlert: PropTypes.func, @@ -397,6 +421,7 @@ const ProjectSaverHOC = function (WrappedComponent) { return { autoSaveTimeoutId: state.scratchGui.timeout.autoSaveTimeoutId, isAnyCreatingNewState: getIsAnyCreatingNewState(loadingState), + isLoading: getIsLoading(loadingState), isCreatingCopy: getIsCreatingCopy(loadingState), isCreatingNew: getIsCreatingNew(loadingState), isRemixing: getIsRemixing(loadingState), @@ -406,6 +431,7 @@ const ProjectSaverHOC = function (WrappedComponent) { isUpdating: getIsUpdating(loadingState), isManualUpdating: getIsManualUpdating(loadingState), loadingState: loadingState, + locale: state.locales.locale, projectChanged: state.scratchGui.projectChanged, reduxProjectId: state.scratchGui.projectState.projectId, reduxProjectTitle: state.scratchGui.projectTitle, diff --git a/test/unit/util/project-saver-hoc.test.jsx b/test/unit/util/project-saver-hoc.test.jsx index 62176a7c5..1336daef0 100644 --- a/test/unit/util/project-saver-hoc.test.jsx +++ b/test/unit/util/project-saver-hoc.test.jsx @@ -22,6 +22,9 @@ describe('projectSaverHOC', () => { timeout: { autoSaveTimeoutId: null } + }, + locales: { + locale: 'en' } }); vm = new VM();