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.
This commit is contained in:
Andrew n marshall
2017-07-11 15:39:35 -07:00
committed by GitHub
parent ca2f0cacf4
commit abcc9b82a1
6 changed files with 335 additions and 35 deletions

View File

@@ -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');
};

View File

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

View File

@@ -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));
}
}

View File

@@ -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');
};
/**

View File

@@ -9,6 +9,7 @@
<script src="../../msg/js/en.js"></script>
<script src="../../blocks_compressed.js"></script>
<script src="../../../closure-library/closure/goog/base.js"></script>
<script src="analytics.js"></script>
<script src="factory_utils.js"></script>
<script src="workspacefactory/wfactory_model.js"></script>
<script src="standard_categories.js"></script>
@@ -32,6 +33,8 @@
<script>
var blocklyFactory;
var init = function() {
BlocklyDevTools.Analytics.init();
blocklyFactory = new AppController();
blocklyFactory.init();
window.addEventListener('beforeunload', blocklyFactory.confirmLeavePage);

View File

@@ -34,8 +34,9 @@
* @author Emma Dauterman (evd2014)
*/
goog.require('FactoryUtils');
goog.require('StandardCategories');
goog.require('BlocklyDevTools.Analytics');
goog.require('FactoryUtils');
goog.require('StandardCategories');
/**
@@ -342,13 +343,25 @@ WorkspaceFactoryController.prototype.exportXmlFile = function(exportMode) {
this.hasUnsavedPreloadChanges = false;
} else {
// Unknown mode. Throw error.
throw new Error ("Unknown export mode: " + exportMode);
var msg = "Unknown export mode: " + exportMode;
BlocklyDevTools.Analytics.onError(msg);
throw new Error(msg);
}
// Download file.
var data = new Blob([configXml], {type: 'text/xml'});
this.view.createAndDownloadFile(fileName, data);
};
if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
BlocklyDevTools.Analytics.onExport(
BlocklyDevTools.Analytics.TOOLBOX,
{ format: BlocklyDevTools.Analytics.FORMAT_XML });
} else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) {
BlocklyDevTools.Analytics.onExport(
BlocklyDevTools.Analytics.WORKSPACE_CONTENTS,
{ format: BlocklyDevTools.Analytics.FORMAT_XML });
}
};
/**
* Export the options object to be used for the Blockly inject call. Gets a
@@ -366,6 +379,13 @@ WorkspaceFactoryController.prototype.exportInjectFile = function() {
var printableOptions = this.generator.generateInjectString()
var data = new Blob([printableOptions], {type: 'text/javascript'});
this.view.createAndDownloadFile(fileName, data);
BlocklyDevTools.Analytics.onExport(
BlocklyDevTools.Analytics.STARTER_CODE,
{
format: BlocklyDevTools.Analytics.FORMAT_JS,
platform: BlocklyDevTools.Analytics.PLATFORM_WEB
});
};
/**
@@ -729,33 +749,45 @@ WorkspaceFactoryController.prototype.importFile = function(file, importMode) {
// Confirm that the user wants to override their current toolbox.
var hasToolboxElements = controller.model.hasElements() ||
controller.toolboxWorkspace.getAllBlocks().length > 0;
if (hasToolboxElements &&
!confirm('Are you sure you want to import? You will lose your ' +
'current toolbox.')) {
return;
if (hasToolboxElements) {
var msg = 'Are you sure you want to import? You will lose your ' +
'current toolbox.';
BlocklyDevTools.Analytics.onWarning(msg);
var continueAnyway = confirm();
if (!continueAnyway) {
return;
}
}
// Import toolbox XML.
controller.importToolboxFromTree_(tree);
BlocklyDevTools.Analytics.onImport('Toolbox.xml');
} else if (importMode == WorkspaceFactoryController.MODE_PRELOAD) {
// Switch mode.
controller.setMode(WorkspaceFactoryController.MODE_PRELOAD);
// Confirm that the user wants to override their current blocks.
if (controller.toolboxWorkspace.getAllBlocks().length > 0 &&
!confirm('Are you sure you want to import? You will lose your ' +
'current workspace blocks.')) {
if (controller.toolboxWorkspace.getAllBlocks().length > 0) {
var msg = 'Are you sure you want to import? You will lose your ' +
'current workspace blocks.';
var continueAnyway = confirm(msg);
BlocklyDevTools.Analytics.onWarning(msg);
if (!continueAnyway) {
return;
}
}
// Import pre-loaded workspace XML.
controller.importPreloadFromTree_(tree);
BlocklyDevTools.Analytics.onImport('WorkspaceContents.xml');
} else {
// Throw error if invalid mode.
throw new Error("Unknown import mode: " + importMode);
}
} catch(e) {
alert('Cannot load XML from file.');
var msg = 'Cannot load XML from file.';
alert(msg);
BlocklyDevTools.Analytics.onError(msg);
console.log(e);
} finally {
Blockly.Events.enable();
@@ -888,8 +920,10 @@ WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
* "Clear" button.
*/
WorkspaceFactoryController.prototype.clearAll = function() {
if (!confirm('Are you sure you want to clear all of your work in Workspace' +
' Factory?')) {
var msg = 'Are you sure you want to clear all of your work in Workspace' +
' Factory?';
BlocklyDevTools.Analytics.onWarning(msg);
if (!confirm(msg)) {
return;
}
var hasCategories = this.model.hasElements();
@@ -1213,11 +1247,15 @@ WorkspaceFactoryController.prototype.importBlocks = function(file, format) {
// If an imported block type is already defined, check if the user wants
// to override the current block definition.
if (controller.model.hasDefinedBlockTypes(blockTypes) &&
!confirm('An imported block uses the same name as a block '
if (controller.model.hasDefinedBlockTypes(blockTypes)) {
var msg = 'An imported block uses the same name as a block '
+ 'already in your toolbox. Are you sure you want to override the '
+ 'currently defined block?')) {
+ 'currently defined block?';
var continueAnyway = confirm(msg);
BlocklyDevTools.Analytics.onWarning(msg);
if (!continueAnyway) {
return;
}
}
var blocks = controller.generator.getDefinedBlocks(blockTypes);
@@ -1234,8 +1272,13 @@ WorkspaceFactoryController.prototype.importBlocks = function(file, format) {
// Reload current category to possibly reflect any newly defined blocks.
controller.clearAndLoadXml_
(Blockly.Xml.workspaceToDom(controller.toolboxWorkspace));
BlocklyDevTools.Analytics.onImport('BlockDefinitions' +
(format == 'JSON' ? '.json' : '.js'));
} catch (e) {
alert('Cannot read blocks from file.');
msg = 'Cannot read blocks from file.';
alert(msg);
BlocklyDevTools.Analytics.onError(msg);
window.console.log(e);
}
}