diff --git a/build.xml b/build.xml index 8f1c25e..2406af8 100755 --- a/build.xml +++ b/build.xml @@ -41,8 +41,7 @@ - - + @@ -71,14 +70,11 @@ - + - - - - - + + diff --git a/source/FeathersSDKManagerContext.as b/source/FeathersSDKManagerContext.as index 6b3cb61..5028d2e 100644 --- a/source/FeathersSDKManagerContext.as +++ b/source/FeathersSDKManagerContext.as @@ -16,11 +16,10 @@ limitations under the License. */ package { - import com.gamua.flox.Flox; + import commands.AnalyticsInitCommand; - import commands.FloxInitCommand; - import commands.FloxLogErrorCommand; - import commands.FloxLogEventInstallCompleteCommand; + import commands.AnalyticsErrorCommand; + import commands.AnalyticsEventInstallCompleteCommand; import events.AcquireProductServiceEventType; import events.RunInstallScriptServiceEventType; @@ -79,12 +78,13 @@ package var applicationVersion:String = applicationDescriptor.ns::versionNumber.toString(); this.injector.mapValue(String, applicationVersion, "applicationVersion"); - CONFIG::USE_FLOX + CONFIG::USE_ANALYTICS { - this.commandMap.mapEvent(ContextEventType.STARTUP_COMPLETE, FloxInitCommand); - this.commandMap.mapEvent(RunInstallScriptServiceEventType.COMPLETE, FloxLogEventInstallCompleteCommand); - this.commandMap.mapEvent(RunInstallScriptServiceEventType.ERROR, FloxLogErrorCommand); - this.commandMap.mapEvent(AcquireProductServiceEventType.ERROR, FloxLogErrorCommand); + this.commandMap.mapEvent(ContextEventType.STARTUP_COMPLETE, AnalyticsInitCommand); + this.commandMap.mapEvent(RunInstallScriptServiceEventType.COMPLETE, AnalyticsEventInstallCompleteCommand); + this.commandMap.mapEvent(RunInstallScriptServiceEventType.ERROR, AnalyticsErrorCommand); + this.commandMap.mapEvent(AcquireProductServiceEventType.ERROR, AnalyticsErrorCommand); + this.commandMap.mapEvent(UncaughtErrorEvent.UNCAUGHT_ERROR, AnalyticsErrorCommand); Starling.current.nativeStage.root.loaderInfo.uncaughtErrorEvents.addEventListener( UncaughtErrorEvent.UNCAUGHT_ERROR, uncaughtErrorEvents_uncaughtErrorEventHandler); } @@ -108,11 +108,11 @@ package if(error is Error) { var errorError:Error = Error(error); - Flox.logError(error, "Uncaught Error: " + errorError.message); + this.dispatchEventWith(UncaughtErrorEvent.UNCAUGHT_ERROR, false, errorError.message); } else { - Flox.logError("UncaughtError", error); + this.dispatchEventWith(UncaughtErrorEvent.UNCAUGHT_ERROR, false, error); } } } diff --git a/source/com/bowlerhatsoftware/analytics/GAMeasurementProtocol.as b/source/com/bowlerhatsoftware/analytics/GAMeasurementProtocol.as new file mode 100644 index 0000000..d8e6f38 --- /dev/null +++ b/source/com/bowlerhatsoftware/analytics/GAMeasurementProtocol.as @@ -0,0 +1,223 @@ +/* +Copyright 2015 Bowler Hat LLC + +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. +*/ +package com.bowlerhatsoftware.analytics +{ + import flash.events.Event; + import flash.events.IOErrorEvent; + import flash.events.SecurityErrorEvent; + import flash.net.URLLoader; + import flash.net.URLRequest; + import flash.net.URLRequestMethod; + import flash.net.URLVariables; + + /** + * A simple implementation of the Google Analytics Measurement Protocol. + */ + public class GAMeasurementProtocol + { + /** + * @private + */ + internal static var loaders:Vector. = new []; + + /** + * @private + */ + private static var PRODUCTION_URL:String = "https://www.google-analytics.com/collect"; + + /** + * @private + */ + private static var DEBUG_URL:String = "https://www.google-analytics.com/debug/collect"; + + /** + * The Google Analytics tracking ID for your property. Has the following + * format: UA-XXXX-Y. Required. + */ + public static var trackingID:String; + + /** + * An anonymous client ID. Required. + */ + public static var clientID:String; + + /** + * The name of your application. + */ + public static var applicationName:String; + + /** + * The version of your application. + */ + public static var applicationVersion:String; + + /** + * When true, the data will be sent to the Google Analytics + * Measurement Protocol Validation Server. The result will be displayed + * in the console. + */ + public static var debugMode:Boolean = false; + + /** + * Tracks an event. + */ + public static function trackEvent(eventCategory:String, eventAction:String, eventLabel:String = null, eventValue:int = -1):void + { + var parameters:URLVariables = createURLVariablesWithDefaults(); + parameters.t = "event"; + parameters.ec = eventCategory; + parameters.ea = eventAction; + if(eventLabel !== null && eventLabel.length > 0) + { + parameters.el = eventLabel; + } + if(eventValue >= 0) + { + parameters.ev = eventValue.toString(); + } + + loadURLRequestWithParameters(parameters); + } + + /** + * Tracks an exception. + */ + public static function trackException(exceptionDescription:String, isFatal:Boolean):void + { + var parameters:URLVariables = createURLVariablesWithDefaults(); + parameters.t = "exception"; + parameters.exd = exceptionDescription; + parameters.exf = isFatal ? "1" : "0"; + + loadURLRequestWithParameters(parameters); + } + + /** + * @private + */ + private static function validateGlobalParameters():void + { + if(GAMeasurementProtocol.trackingID === null || GAMeasurementProtocol.trackingID.length === 0) + { + throw new ArgumentError("MeasurementProtocol.trackingID cannot be null.") + } + if(GAMeasurementProtocol.clientID === null || GAMeasurementProtocol.clientID.length === 0) + { + throw new ArgumentError("MeasurementProtocol.clientID cannot be null.") + } + } + + /** + * @private + */ + private static function createURLVariablesWithDefaults():URLVariables + { + validateGlobalParameters(); + var parameters:URLVariables = new URLVariables(); + parameters.v = "1"; + parameters.tid = trackingID; + parameters.cid = clientID; + if(applicationName !== null && applicationName.length > 0) + { + parameters.an = applicationName; + } + if(applicationVersion !== null && applicationVersion.length > 0) + { + parameters.av = applicationVersion; + } + return parameters; + } + + /** + * @private + */ + private static function loadURLRequestWithParameters(parameters:URLVariables):void + { + var url:String = debugMode ? DEBUG_URL : PRODUCTION_URL; + var request:URLRequest = new URLRequest(url); + request.method = URLRequestMethod.POST; + request.data = parameters; + + var loader:URLLoader = new URLLoader(); + loader.addEventListener(Event.COMPLETE, loader_completeHandler); + loader.addEventListener(IOErrorEvent.IO_ERROR, loader_ioErrorHandler); + loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loader_securityErrorHandler); + loaders[loaders.length] = loader; + loader.load(request); + } + + /** + * @private + */ + private static function cleanupLoader(loader:URLLoader):void + { + loader.removeEventListener(Event.COMPLETE, loader_completeHandler); + loader.removeEventListener(IOErrorEvent.IO_ERROR, loader_ioErrorHandler); + var index:int = loaders.indexOf(loader); + if(index === 0) + { + loaders.shift(); + } + else if(index === loaders.length - 1) + { + loaders.pop(); + } + else + { + loaders.splice(index, 1); + } + } + + /** + * @private + */ + private static function loader_completeHandler(event:Event):void + { + var loader:URLLoader = URLLoader(event.currentTarget); + cleanupLoader(loader); + if(debugMode) + { + trace(loader.data); + } + } + + /** + * @private + */ + private static function loader_ioErrorHandler(event:IOErrorEvent):void + { + var loader:URLLoader = URLLoader(event.currentTarget); + cleanupLoader(loader); + if(debugMode) + { + trace(event); + } + } + + /** + * @private + */ + private static function loader_securityErrorHandler(event:SecurityErrorEvent):void + { + var loader:URLLoader = URLLoader(event.currentTarget); + cleanupLoader(loader); + if(debugMode) + { + trace(event); + } + } + } +} diff --git a/source/commands/FloxLogErrorCommand.as b/source/commands/AnalyticsErrorCommand.as similarity index 75% rename from source/commands/FloxLogErrorCommand.as rename to source/commands/AnalyticsErrorCommand.as index 0ac7846..bccd385 100644 --- a/source/commands/FloxLogErrorCommand.as +++ b/source/commands/AnalyticsErrorCommand.as @@ -16,15 +16,14 @@ limitations under the License. */ package commands { - import com.gamua.flox.Flox; - import model.SDKManagerModel; + import com.bowlerhatsoftware.analytics.GAMeasurementProtocol; import org.robotlegs.starling.mvcs.Command; import starling.events.Event; - public class FloxLogErrorCommand extends Command + public class AnalyticsErrorCommand extends Command { [Inject] public var event:Event; @@ -34,8 +33,9 @@ package commands override public function execute():void { - var message:String = event.data as String; - Flox.logError(this.event.type, message) + var errorType:String = this.event.type; + var message:String = this.event.data as String; + GAMeasurementProtocol.trackException(errorType + ": " + message, true); } } } diff --git a/source/commands/FloxLogEventInstallCompleteCommand.as b/source/commands/AnalyticsEventInstallCompleteCommand.as similarity index 69% rename from source/commands/FloxLogEventInstallCompleteCommand.as rename to source/commands/AnalyticsEventInstallCompleteCommand.as index c03f8fa..49f31f7 100644 --- a/source/commands/FloxLogEventInstallCompleteCommand.as +++ b/source/commands/AnalyticsEventInstallCompleteCommand.as @@ -16,31 +16,22 @@ limitations under the License. */ package commands { - import com.gamua.flox.Flox; - import model.SDKManagerModel; + import com.bowlerhatsoftware.analytics.GAMeasurementProtocol; + import org.robotlegs.starling.mvcs.Command; - public class FloxLogEventInstallCompleteCommand extends Command + public class AnalyticsEventInstallCompleteCommand extends Command { - private static const EVENT_NAME:String = "InstallComplete"; - [Inject] public var sdkManagerModel:SDKManagerModel; override public function execute():void { var productVersion:String = sdkManagerModel.selectedProduct.versionNumber; - var runtimeVersion:String = sdkManagerModel.selectedRuntime.airVersionNumber; var operatingSystem:String = sdkManagerModel.operatingSystem; - Flox.logEvent(EVENT_NAME, - { - productVersion: productVersion, - runtimeVersion: runtimeVersion, - operatingSystem: operatingSystem - }); - Flox.shutdown(); + GAMeasurementProtocol.trackEvent("InstallComplete", operatingSystem, productVersion); } } } diff --git a/source/commands/AnalyticsInitCommand.as b/source/commands/AnalyticsInitCommand.as new file mode 100644 index 0000000..3d02ef1 --- /dev/null +++ b/source/commands/AnalyticsInitCommand.as @@ -0,0 +1,36 @@ +package commands +{ + import flash.net.SharedObject; + + import mx.utils.UIDUtil; + + import com.bowlerhatsoftware.analytics.GAMeasurementProtocol; + import org.robotlegs.starling.mvcs.Command; + + public class AnalyticsInitCommand extends Command + { + [Inject(name="applicationVersion")] + public var applicationVersion:String; + + override public function execute():void + { + GAMeasurementProtocol.debugMode = false; + GAMeasurementProtocol.trackingID = CONFIG::ANALYTICS_TRACKING_ID; + GAMeasurementProtocol.clientID = getUID(); + GAMeasurementProtocol.applicationName = "Feathers SDK Manager"; + GAMeasurementProtocol.applicationVersion = applicationVersion; + } + + private function getUID():String + { + var so:SharedObject = SharedObject.getLocal("uid"); + var uid:String = so.data.uid; + if(uid === null) + { + so.data.uid = uid = UIDUtil.createUID(); + so.flush(); + } + return uid; + } + } +} diff --git a/source/commands/FloxInitCommand.as b/source/commands/FloxInitCommand.as deleted file mode 100644 index 9a6fe00..0000000 --- a/source/commands/FloxInitCommand.as +++ /dev/null @@ -1,33 +0,0 @@ -/* -Feathers SDK Manager -Copyright 2015 Bowler Hat LLC - -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. -*/ -package commands -{ - import com.gamua.flox.Flox; - - import org.robotlegs.starling.mvcs.Command; - - public class FloxInitCommand extends Command - { - [Inject(name="applicationVersion")] - public var applicationVersion:String; - - override public function execute():void - { - Flox.init(CONFIG::FLOX_ID, CONFIG::FLOX_KEY, this.applicationVersion); - } - } -}