From abcc9b82a16ddcba3650d9590cffab18e4f28e38 Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Tue, 11 Jul 2017 15:39:35 -0700 Subject: [PATCH] Adding BlocklyDevTools.Analytics (#1217) Adding BlocklyDevTools.Analytics, an interface for integrating an analytics library to track basic usage, including: * navigation. * saving, importing, and exporting. * warnings and errors. --- demos/blockfactory/analytics.js | 210 ++++++++++++++++++ demos/blockfactory/app_controller.js | 43 +++- .../blockfactory/block_exporter_controller.js | 20 +- .../blockfactory/block_library_controller.js | 15 +- demos/blockfactory/index.html | 3 + .../workspacefactory/wfactory_controller.js | 79 +++++-- 6 files changed, 335 insertions(+), 35 deletions(-) create mode 100644 demos/blockfactory/analytics.js diff --git a/demos/blockfactory/analytics.js b/demos/blockfactory/analytics.js new file mode 100644 index 000000000..6473f8858 --- /dev/null +++ b/demos/blockfactory/analytics.js @@ -0,0 +1,210 @@ +/** + * @license + * Blockly Demos: Block Factory + * + * Copyright 2017 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Stubbed interface functions for analytics integration. + */ + +goog.provide('BlocklyDevTools.Analytics'); + +/** + * Whether these stub methods should log analytics calls to the console. + * @private + * @const + */ +BlocklyDevTools.Analytics.LOG_TO_CONSOLE_ = false; + +/** + * An import/export type id for a library of BlockFactory's original block + * save files (each a serialized workspace of block definition blocks). + * @package + * @const + */ +BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY = "Block Factory library"; +/** + * An import/export type id for a standard Blockly library of block + * definitions. + * @package + * @const + */ +BlocklyDevTools.Analytics.BLOCK_DEFINITIONS = "Block definitions"; +/** + * An import/export type id for a code generation function, or a + * boilerplate stub of the same. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.GENERATOR = "Generator"; +/** + * An import/export type id for a Blockly Toolbox. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.TOOLBOX = "Toolbox"; +/** + * An import/export type id for the serialized contents of a workspace. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.WORKSPACE_CONTENTS = "Workspace contents"; + +/** + * Format id for imported/exported JavaScript resources. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.FORMAT_JS = "JavaScript"; +/** + * Format id for imported/exported JSON resources. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.FORMAT_JSON = "JSON"; +/** + * Format id for imported/exported XML resources. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.FORMAT_XML = "XML"; + +/** + * Platform id for resources exported for use in Android projects. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.PLATFORM_ANDROID = "Android"; +/** + * Platform id for resources exported for use in iOS projects. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.PLATFORM_IOS = "iOS"; +/** + * Platform id for resources exported for use in web projects. + * + * @package + * @const + */ +BlocklyDevTools.Analytics.PLATFORM_WEB = "web"; + +/** + * Initializes the analytics framework, including noting that the page/app was + * opened. + * @package + */ +BlocklyDevTools.Analytics.init = function() { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.init'); +}; + +/** + * Event noting the user navigated to a specific view. + * + * @package + * @param viewId {string} An identifier for the view state. + */ +BlocklyDevTools.Analytics.onNavigateTo = function(viewId) { + // stub + this.LOG_TO_CONSOLE_ && + console.log('Analytics.onNavigateTo(' + viewId + ')'); +}; + +/** + * Event noting a project resource was saved. In the web Block Factory, this + * means saved to localStorage. + * + * @package + * @param typeId {string} An identifying string for the saved type. + */ +BlocklyDevTools.Analytics.onSave = function(typeId) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onSave(' + typeId + ')'); +}; + +/** + * Event noting the user attempted to import a resource file. + * + * @package + * @param typeId {string} An identifying string for the imported type. + * @param optMetadata {Object} Metadata about the import, such as format and + * platform. + */ +BlocklyDevTools.Analytics.onImport = function(typeId, optMetadata) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onImport(' + typeId + + (optMetadata ? '): ' + JSON.stringify(optMetadata) : ')')); +}; + +/** + * Event noting a project resource was saved. In the web Block Factory, this + * means downloaded to the user's system. + * + * @package + * @param typeId {string} An identifying string for the exported object type. + * @param optMetadata {Object} Metadata about the import, such as format and + * platform. + */ +BlocklyDevTools.Analytics.onExport = function(typeId, optMetadata) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onExport(' + typeId + + (optMetadata ? '): ' + JSON.stringify(optMetadata) : ')')); +}; + +/** + * Event noting the system encountered an error. It should attempt to send + * immediately. + * + * @package + * @param e {!Object} A value representing or describing the error. + */ +BlocklyDevTools.Analytics.onError = function(e) { + // stub + this.LOG_TO_CONSOLE_ && + console.log('Analytics.onError("' + e.toString() + '")'); +}; + +/** + * Event noting the user was notified with a warning. + * + * @package + * @param msg {string} The warning message, or a description thereof. + */ +BlocklyDevTools.Analytics.onWarning = function(msg) { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.onWarning("' + msg + '")'); +}; + +/** + * Request the analytics framework to send any queued events to the server. + * @package + */ +BlocklyDevTools.Analytics.sendQueued = function() { + // stub + this.LOG_TO_CONSOLE_ && console.log('Analytics.sendQueued'); +}; + diff --git a/demos/blockfactory/app_controller.js b/demos/blockfactory/app_controller.js index ac69780b2..e3fc1bd23 100644 --- a/demos/blockfactory/app_controller.js +++ b/demos/blockfactory/app_controller.js @@ -28,6 +28,7 @@ goog.provide('AppController'); goog.require('BlockFactory'); +goog.require('BlocklyDevTools.Analytics'); goog.require('FactoryUtils'); goog.require('BlockLibraryController'); goog.require('BlockExporterController'); @@ -85,6 +86,10 @@ AppController.prototype.importBlockLibraryFromFile = function() { var files = document.getElementById('files'); // If the file list is empty, the user likely canceled in the dialog. if (files.files.length > 0) { + BlocklyDevTools.Analytics.onImport( + BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY, + { format: BlocklyDevTools.Analytics.FORMAT_XML }); + // The input tag doesn't have the "multiple" attribute // so the user can only choose 1 file. var file = files.files[0]; @@ -137,9 +142,14 @@ AppController.prototype.exportBlockLibraryToFile = function() { // Download file if all necessary parameters are provided. if (filename) { FactoryUtils.createAndDownloadFile(blockLibText, filename, 'xml'); + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY, + { format: BlocklyDevTools.Analytics.FORMAT_XML }); } else { - alert('Could not export Block Library without file name under which to ' + - 'save library.'); + var msg = 'Could not export Block Library without file name under which ' + + 'to save library.'; + BlocklyDevTools.Analytics.onWarning(msg); + alert(msg); } }; @@ -201,7 +211,7 @@ AppController.prototype.formatBlockLibraryForImport_ = function(xmlText) { var blockType = this.getBlockTypeFromXml_(xmlText).toLowerCase(); // Some names are invalid so fix them up. blockType = FactoryUtils.cleanBlockType(blockType); - + blockXmlTextMap[blockType] = xmlText; } @@ -285,13 +295,17 @@ AppController.prototype.onTab = function() { var hasUnsavedChanges = !FactoryUtils.savedBlockChanges(this.blockLibraryController); - if (hasUnsavedChanges && - !confirm('You have unsaved changes in Block Factory.')) { - // If the user doesn't want to switch tabs with unsaved changes, - // stay on Block Factory Tab. - this.setSelected_(AppController.BLOCK_FACTORY); - this.lastSelectedTab = AppController.BLOCK_FACTORY; - return; + if (hasUnsavedChanges) { + var msg = 'You have unsaved changes in Block Factory.'; + var continueAnyway = confirm(msg); + BlocklyDevTools.Analytics.onWarning(msg); + if (!continueAnyway) { + // If the user doesn't want to switch tabs with unsaved changes, + // stay on Block Factory Tab. + this.setSelected_(AppController.BLOCK_FACTORY); + this.lastSelectedTab = AppController.BLOCK_FACTORY; + return; + } } } @@ -304,6 +318,8 @@ AppController.prototype.onTab = function() { this.styleTabs_(); if (this.selectedTab == AppController.EXPORTER) { + BlocklyDevTools.Analytics.onNavigateTo('Exporter'); + // Hide other tabs. FactoryUtils.hide('workspaceFactoryContent'); FactoryUtils.hide('blockFactoryContent'); @@ -325,6 +341,8 @@ AppController.prototype.onTab = function() { this.exporter.updatePreview(); } else if (this.selectedTab == AppController.BLOCK_FACTORY) { + BlocklyDevTools.Analytics.onNavigateTo('BlockFactory'); + // Hide other tabs. FactoryUtils.hide('blockLibraryExporter'); FactoryUtils.hide('workspaceFactoryContent'); @@ -332,6 +350,9 @@ AppController.prototype.onTab = function() { FactoryUtils.show('blockFactoryContent'); } else if (this.selectedTab == AppController.WORKSPACE_FACTORY) { + // TODO: differentiate Workspace and Toolbox editor, based on the other tab state. + BlocklyDevTools.Analytics.onNavigateTo('WorkspaceFactory'); + // Hide other tabs. FactoryUtils.hide('blockLibraryExporter'); FactoryUtils.hide('blockFactoryContent'); @@ -624,12 +645,14 @@ AppController.prototype.onresize = function(event) { * @param {!Event} e beforeunload event. */ AppController.prototype.confirmLeavePage = function(e) { + BlocklyDevTools.Analytics.sendQueued(); if ((!BlockFactory.isStarterBlock() && !FactoryUtils.savedBlockChanges(blocklyFactory.blockLibraryController)) || blocklyFactory.workspaceFactoryController.hasUnsavedChanges()) { var confirmationMessage = 'You will lose any unsaved changes. ' + 'Are you sure you want to exit this page?'; + BlocklyDevTools.Analytics.onWarning(confirmationMessage); e.returnValue = confirmationMessage; return confirmationMessage; } diff --git a/demos/blockfactory/block_exporter_controller.js b/demos/blockfactory/block_exporter_controller.js index b237f48a7..705de2667 100644 --- a/demos/blockfactory/block_exporter_controller.js +++ b/demos/blockfactory/block_exporter_controller.js @@ -31,6 +31,7 @@ goog.provide('BlockExporterController'); +goog.require('BlocklyDevTools.Analytics'); goog.require('FactoryUtils'); goog.require('StandardCategories'); goog.require('BlockExporterView'); @@ -103,7 +104,9 @@ BlockExporterController.prototype.export = function() { // User wants to export selected blocks' definitions. if (!blockDef_filename) { // User needs to enter filename. - alert('Please enter a filename for your block definition(s) download.'); + var msg = 'Please enter a filename for your block definition(s) download.'; + BlocklyDevTools.Analytics.onWarning(msg); + alert(msg); } else { // Get block definition code in the selected format for the blocks. var blockDefs = this.tools.getBlockDefinitions(blockXmlMap, @@ -111,6 +114,13 @@ BlockExporterController.prototype.export = function() { // Download the file, using .js file ending for JSON or Javascript. FactoryUtils.createAndDownloadFile( blockDefs, blockDef_filename, 'javascript'); + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.BLOCK_DEFINITIONS, + { + format: (definitionFormat == 'JSON' ? + BlocklyDevTools.Analytics.FORMAT_JSON : + BlocklyDevTools.Analytics.FORMAT_JS) + }); } } @@ -118,7 +128,9 @@ BlockExporterController.prototype.export = function() { // User wants to export selected blocks' generator stubs. if (!generatorStub_filename) { // User needs to enter filename. - alert('Please enter a filename for your generator stub(s) download.'); + var msg = 'Please enter a filename for your generator stub(s) download.'; + BlocklyDevTools.Analytics.onWarning(msg); + alert(msg); } else { // Get generator stub code in the selected language for the blocks. var genStubs = this.tools.getGeneratorCode(blockXmlMap, @@ -128,6 +140,10 @@ BlockExporterController.prototype.export = function() { // Download the file. FactoryUtils.createAndDownloadFile( genStubs, generatorStub_filename, fileType); + BlocklyDevTools.Analytics.onExport( + BlocklyDevTools.Analytics.GENERATOR, + (fileType == 'javascript' ? + { format: BlocklyDevTools.Analytics.FORMAT_JS } : undefined)); } } diff --git a/demos/blockfactory/block_library_controller.js b/demos/blockfactory/block_library_controller.js index 1066ab127..b0e540489 100644 --- a/demos/blockfactory/block_library_controller.js +++ b/demos/blockfactory/block_library_controller.js @@ -34,6 +34,7 @@ goog.provide('BlockLibraryController'); +goog.require('BlocklyDevTools.Analytics'); goog.require('BlockLibraryStorage'); goog.require('BlockLibraryView'); goog.require('BlockFactory'); @@ -110,8 +111,9 @@ BlockLibraryController.prototype.getSelectedBlockType = function() { * updating the dropdown and displaying the starter block (factory_base). */ BlockLibraryController.prototype.clearBlockLibrary = function() { - var check = confirm('Delete all blocks from library?'); - if (check) { + var msg = 'Delete all blocks from library?'; + BlocklyDevTools.Analytics.onWarning(msg); + if (confirm(msg)) { // Clear Block Library Storage. this.storage.clear(); this.storage.saveToLocalStorage(); @@ -133,9 +135,11 @@ BlockLibraryController.prototype.saveToBlockLibrary = function() { // If user has not changed the name of the starter block. if (blockType == 'block_type') { // Do not save block if it has the default type, 'block_type'. - alert('You cannot save a block under the name "block_type". Try changing ' + - 'the name before saving. Then, click on the "Block Library" button ' + - 'to view your saved blocks.'); + var msg = 'You cannot save a block under the name "block_type". Try ' + + 'changing the name before saving. Then, click on the "Block Library"' + + ' button to view your saved blocks.'; + alert(msg); + BlocklyDevTools.Analytics.onWarning(msg); return; } @@ -159,6 +163,7 @@ BlockLibraryController.prototype.saveToBlockLibrary = function() { // Add select handler to the new option. this.addOptionSelectHandler(blockType); + BlocklyDevTools.Analytics.onSave('Block'); }; /** diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html index 2b49f7ef2..9190bd132 100644 --- a/demos/blockfactory/index.html +++ b/demos/blockfactory/index.html @@ -9,6 +9,7 @@ + @@ -32,6 +33,8 @@