diff --git a/libraries/analytics.js b/libraries/analytics.js new file mode 100644 index 00000000..0dae3ea9 --- /dev/null +++ b/libraries/analytics.js @@ -0,0 +1,58 @@ +(function(){var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b\x3c/script>')):(c=M.createElement("script"), +c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},be=function(a,b){return E(M.location[b?"href":"search"],a)},E=function(a,b){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},de=function(a,b){var c=a.indexOf(b);if(5==c||6==c)if(a=a.charAt(c+b.length),"/"==a||"?"==a|| +""==a||":"==a)return!0;return!1},ya=function(a,b){var c=M.referrer;if(/^(https?|android-app):\/\//i.test(c)){if(a)return c;a="//"+M.location.hostname;if(!de(c,a))return b&&(b=a.replace(/\./g,"-")+".cdn.ampproject.org",de(c,b))?void 0:c}},za=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e=b.length)wc(a,b,c);else if(8192>=b.length)x(a,b,c)||wd(a,b,c)||wc(a,b,c);else throw ge("len",b.length),new Da(b.length);},pe=function(a,b,c,d){d=d||ua;wd(a+"?"+b,"",d,c)},wc=function(a,b,c){var d=ta(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},wd=function(a,b,c,d){var e=O.XMLHttpRequest; +if(!e)return!1;var g=new e;if(!("withCredentials"in g))return!1;a=a.replace(/^http:/,"https:");g.open("POST",a,!0);g.withCredentials=!0;g.setRequestHeader("Content-Type","text/plain");g.onreadystatechange=function(){if(4==g.readyState){if(d)try{var a=g.responseText;if(1>a.length)ge("xhr","ver","0"),c();else if("1"!=a.charAt(0))ge("xhr","ver",String(a.length)),c();else if(3=100*R(a,Ka))throw"abort";}function Ma(a){if(G(P(a,Na)))throw"abort";}function Oa(){var a=M.location.protocol;if("http:"!=a&&"https:"!=a)throw"abort";} +function Pa(a){try{O.navigator.sendBeacon?J(42):O.XMLHttpRequest&&"withCredentials"in new O.XMLHttpRequest&&J(40)}catch(c){}a.set(ld,Td(a),!0);a.set(Ac,R(a,Ac)+1);var b=[];Qa.map(function(c,d){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});b.push("z="+Bd());a.set(Ra,b.join("&"),!0)} +function Sa(a){var b=P(a,gd)||oe()+"/collect",c=a.get(qe),d=P(a,fa);!d&&a.get(Vd)&&(d="beacon");if(c)pe(b,P(a,Ra),c,a.get(Ia));else if(d){c=d;d=P(a,Ra);var e=a.get(Ia);e=e||ua;"image"==c?wc(b,d,e):"xhr"==c&&wd(b,d,e)||"beacon"==c&&x(b,d,e)||ba(b,d,e)}else ba(b,P(a,Ra),a.get(Ia));b=a.get(Na);b=h(b);c=b.hitcount;b.hitcount=c?c+1:1;b=a.get(Na);delete h(b).pending_experiments;a.set(Ia,ua,!0)} +function Hc(a){(O.gaData=O.gaData||{}).expId&&a.set(Nc,(O.gaData=O.gaData||{}).expId);(O.gaData=O.gaData||{}).expVar&&a.set(Oc,(O.gaData=O.gaData||{}).expVar);var b=a.get(Na);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&a.set(m,d,!0)}function cd(){if(O.navigator&&"preview"==O.navigator.loadPurpose)throw"abort";} +function yd(a){var b=O.gaDevIds;ka(b)&&0!=b.length&&a.set("&did",b.join(","),!0)}function vb(a){if(!a.get(Na))throw"abort";};var hd=function(){return Math.round(2147483647*Math.random())},Bd=function(){try{var a=new Uint32Array(1);O.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return hd()}};function Ta(a){var b=R(a,Ua);500<=b&&J(15);var c=P(a,Va);if("transaction"!=c&&"item"!=c){c=R(a,Wa);var d=(new Date).getTime(),e=R(a,Xa);0==e&&a.set(Xa,d);e=Math.round(2*(d-e)/1E3);0=c)throw"abort";a.set(Wa,--c)}a.set(Ua,++b)};var Ya=function(){this.data=new ee},Qa=new ee,Za=[];Ya.prototype.get=function(a){var b=$a(a),c=this.data.get(a);b&&void 0==c&&(c=ea(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.Z?b.Z(this,a,c):c};var P=function(a,b){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};Ya.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&ab(this,d,a[d],c);else ab(this,a,b,c)}; +var ab=function(a,b,c,d){if(void 0!=c)switch(b){case Na:wb.test(c)}var e=$a(b);e&&e.o?e.o(a,b,c,d):a.data.set(b,c,d)},bb=function(a,b,c,d,e){this.name=a;this.F=b;this.Z=d;this.o=e;this.defaultValue=c},$a=function(a){var b=Qa.get(a);if(!b)for(var c=0;c=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0c)a[b]=void 0},Fd=function(a){return function(b){if("pageview"==b.get(Va)&& +!a.I){a.I=!0;var c=aa(b),d=0a.length)J(12);else{for(var d=[],e=0;e=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()|| +!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());a.data.set(ce,be("gclid",!0));a.data.set(ie,be("gclsrc",!0));a.data.set(fe,Math.round((new Date).getTime()/1E3));if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;carguments.length)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else b=arguments[0]&&arguments[0][Va],c=arguments;b&&(c=za(qc[b]||[],c),c[Va]=b,this.b.set(c,void 0,!0),this.filters.D(this.b),this.b.data.m={})}}; +pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var td=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=td.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.C=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.C,this.i="require"==this.C,this.g="provide"==this.C,this.ba="remove"==this.C),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.C)throw"abort";if(this.i&&(!qa(b)||""==b))throw"abort"; +if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47}; +var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);if(p.test(b)){J(52);a=N.j(a);if(!a)return!0;c=d||{};d={id:b,B:c.dataLayer||"dataLayer",ia:!!a.get("anonymizeIp"),sync:e,G:!1};a.get(">m")==b&&(d.G=!0);var g=String(a.get("name"));"t0"!=g&&(d.target=g);G(String(a.get("trackingId")))||(d.clientId= +String(a.get(Q)),d.ka=Number(a.get(n)),c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,d.la=c,d.qa=E(a.b.get(kb)||"","gclid"));a=d.B;c=(new Date).getTime();O[a]=O[a]||[];c={"gtm.start":c};e||(c.event="gtm.js");O[a].push(c);c=t(d)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);c&&(c&&0<=c.indexOf("/")||(c=(Ba||"https:"==M.location.protocol?"https:":"http:")+"//www.google-analytics.com/plugins/ua/"+c),d=ae(c),a=d.protocol,c=M.location.protocol, +("https:"==a||a==c||("http:"!=a?0:"http:"==c))&&B(d)&&(wa(d.url,void 0,e),$d.set(b,!0)))}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);b=A.get(a)||[];for(var c=0;ca.split("/")[0].indexOf(":")&& +(a=ca+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments);b=Z.f.concat(b);for(Z.f=[];0c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){b= +!0;break a}}b=!1}b&&(Ba=!0)}(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};N.da=function(){for(var a=N.getAll(),b=0;b>21:b}return b};})(window); diff --git a/locales/en/messages.json b/locales/en/messages.json index 65685851..f5f18aac 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -697,6 +697,9 @@ "defaultDiscordText": { "message": "Betaflight Discord Server.
Share your flight experience, talk about Betaflight, help other people or get some help for yourself from the community." }, + "statisticsDisclaimer": { + "message": "Betaflight Configurator collects anonymous usage statistics. For example, this data includes (but is not limited to) the number of launches, geographical region of the users, types of flight controllers, firmware versions, usage of UI elements and tabs, etc. The summary of this data is shared here. Collection is done in order to better understand how Betaflight Configurator is being used, to understand community trends, and for possible UI improvements. Users can opt-out of data collection in the Options tab." + }, "defaultChangelogHead": { "message": "Configurator - Changelog" }, diff --git a/src/js/Analytics.js b/src/js/Analytics.js new file mode 100644 index 00000000..b4a718c1 --- /dev/null +++ b/src/js/Analytics.js @@ -0,0 +1,180 @@ +'use strict'; + +const Analytics = function (trackingId, userId, appName, appVersion, gitRevision, os, checkForDebugVersions, optOut, debugMode, buildType) { + this._trackingId = trackingId; + + this.setOptOut(optOut); + + this._googleAnalytics = googleAnalytics; + + this._googleAnalytics.initialize(this._trackingId, { + storage: 'none', + clientId: userId, + debug: !!debugMode, + }); + + // Make it work for the Chrome App: + this._googleAnalytics.set('forceSSL', true); + this._googleAnalytics.set('transport', 'xhr'); + + // Make it work for NW.js: + this._googleAnalytics.set('checkProtocolTask', null); + + this._googleAnalytics.set('appName', appName); + this._googleAnalytics.set('appVersion', debugMode ? `${appVersion}-debug` : appVersion); + + this.EVENT_CATEGORIES = { + APPLICATION: 'Application', + FLIGHT_CONTROLLER: 'FlightController', + FLASHING: 'Flashing', + }; + + this.DATA = { + BOARD_TYPE: 'boardType', + API_VERSION: 'apiVersion', + FIRMWARE_TYPE: 'firmwareType', + FIRMWARE_VERSION: 'firmwareVersion', + FIRMWARE_NAME: 'firmwareName', + FIRMWARE_SOURCE: 'firmwareSource', + FIRMWARE_CHANNEL: 'firmwareChannel', + FIRMWARE_ERASE_ALL: 'firmwareEraseAll', + FIRMWARE_SIZE: 'firmwareSize', + MCU_ID: 'mcuId', + LOGGING_STATUS: 'loggingStatus', + LOG_SIZE: 'logSize', + TARGET_NAME: 'targetName', + BOARD_NAME: 'boardName', + MANUFACTURER_ID: 'manufacturerId', + MCU_TYPE: 'mcuType', + }; + + this.DIMENSIONS = { + CONFIGURATOR_OS: 1, + BOARD_TYPE: 2, + FIRMWARE_TYPE: 3, + FIRMWARE_VERSION: 4, + API_VERSION: 5, + FIRMWARE_NAME: 6, + FIRMWARE_SOURCE: 7, + FIRMWARE_ERASE_ALL: 8, + CONFIGURATOR_EXPERT_MODE: 9, + FIRMWARE_CHANNEL: 10, + LOGGING_STATUS: 11, + MCU_ID: 12, + CONFIGURATOR_CHANGESET_ID: 13, + CONFIGURATOR_USE_DEBUG_VERSIONS: 14, + TARGET_NAME: 15, + BOARD_NAME: 16, + MANUFACTURER_ID: 17, + MCU_TYPE: 18, + CONFIGURATOR_BUILD_TYPE: 19, + }; + + this.METRICS = { + FIRMWARE_SIZE: 1, + LOG_SIZE: 2, + }; + + this.setDimension(this.DIMENSIONS.CONFIGURATOR_OS, os); + this.setDimension(this.DIMENSIONS.CONFIGURATOR_CHANGESET_ID, gitRevision); + this.setDimension(this.DIMENSIONS.CONFIGURATOR_USE_DEBUG_VERSIONS, checkForDebugVersions); + this.setDimension(this.DIMENSIONS.CONFIGURATOR_BUILD_TYPE, buildType); + + this.resetFlightControllerData(); + this.resetFirmwareData(); +}; + +Analytics.prototype.setDimension = function (dimension, value) { + const dimensionName = `dimension${dimension}`; + this._googleAnalytics.custom(dimensionName, value); +}; + +Analytics.prototype.setMetric = function (metric, value) { + const metricName = `metric${metric}`; + this._googleAnalytics.custom(metricName, value); +}; + +Analytics.prototype.sendEvent = function (category, action, options) { + this._googleAnalytics.event(category, action, options); +}; + +Analytics.prototype.sendChangeEvents = function (category, changeList) { + for (const actionName in changeList) { + if (changeList.hasOwnProperty(actionName)) { + const actionValue = changeList[actionName]; + if (actionValue !== undefined) { + this.sendEvent(category, actionName, { eventLabel: actionValue }); + } + } + } +}; + +Analytics.prototype.sendSaveAndChangeEvents = function (category, changeList, tabName) { + this.sendEvent(category, 'Save', { + eventLabel: tabName, + eventValue: Object.keys(changeList).length, + }); + this.sendChangeEvents(category, changeList); +}; + +Analytics.prototype.sendAppView = function (viewName) { + this._googleAnalytics.screenview(viewName); +}; + +Analytics.prototype.sendTiming = function (category, timing, value) { + this._googleAnalytics.timing(category, timing, value); +}; + +Analytics.prototype.sendException = function (message) { + this._googleAnalytics.exception(message); +}; + +Analytics.prototype.setOptOut = function (optOut) { + window[`ga-disable-${this._trackingId}`] = !!optOut; +}; + +Analytics.prototype._rebuildFlightControllerEvent = function () { + this.setDimension(this.DIMENSIONS.BOARD_TYPE, this._flightControllerData[this.DATA.BOARD_TYPE]); + this.setDimension(this.DIMENSIONS.FIRMWARE_TYPE, this._flightControllerData[this.DATA.FIRMWARE_TYPE]); + this.setDimension(this.DIMENSIONS.FIRMWARE_VERSION, this._flightControllerData[this.DATA.FIRMWARE_VERSION]); + this.setDimension(this.DIMENSIONS.API_VERSION, this._flightControllerData[this.DATA.API_VERSION]); + this.setDimension(this.DIMENSIONS.LOGGING_STATUS, this._flightControllerData[this.DATA.LOGGING_STATUS]); + this.setDimension(this.DIMENSIONS.MCU_ID, this._flightControllerData[this.DATA.MCU_ID]); + this.setMetric(this.METRICS.LOG_SIZE, this._flightControllerData[this.DATA.LOG_SIZE]); + this.setDimension(this.DIMENSIONS.TARGET_NAME, this._flightControllerData[this.DATA.TARGET_NAME]); + this.setDimension(this.DIMENSIONS.BOARD_NAME, this._flightControllerData[this.DATA.BOARD_NAME]); + this.setDimension(this.DIMENSIONS.MANUFACTURER_ID, this._flightControllerData[this.DATA.MANUFACTURER_ID]); + this.setDimension(this.DIMENSIONS.MCU_TYPE, this._flightControllerData[this.DATA.MCU_TYPE]); +}; + +Analytics.prototype.setFlightControllerData = function (property, value) { + this._flightControllerData[property] = value; + + this._rebuildFlightControllerEvent(); +}; + +Analytics.prototype.resetFlightControllerData = function () { + this._flightControllerData = {}; + + this._rebuildFlightControllerEvent(); +}; + +Analytics.prototype._rebuildFirmwareEvent = function () { + this.setDimension(this.DIMENSIONS.FIRMWARE_NAME, this._firmwareData[this.DATA.FIRMWARE_NAME]); + this.setDimension(this.DIMENSIONS.FIRMWARE_SOURCE, this._firmwareData[this.DATA.FIRMWARE_SOURCE]); + this.setDimension(this.DIMENSIONS.FIRMWARE_ERASE_ALL, this._firmwareData[this.DATA.FIRMWARE_ERASE_ALL]); + this.setDimension(this.DIMENSIONS.FIRMWARE_CHANNEL, this._firmwareData[this.DATA.FIRMWARE_CHANNEL]); + this.setMetric(this.METRICS.FIRMWARE_SIZE, this._firmwareData[this.DATA.FIRMWARE_SIZE]); +}; + +Analytics.prototype.setFirmwareData = function (property, value) { + this._firmwareData[property] = value; + + this._rebuildFirmwareEvent(); +}; + +Analytics.prototype.resetFirmwareData = function () { + this._firmwareData = {}; + + this._rebuildFirmwareEvent(); +}; diff --git a/src/js/CliAutoComplete.js b/src/js/CliAutoComplete.js index d03374ba..fff2866f 100644 --- a/src/js/CliAutoComplete.js +++ b/src/js/CliAutoComplete.js @@ -51,6 +51,8 @@ CliAutoComplete.setEnabled = function(enable) { }; CliAutoComplete.initialize = function($textarea, sendLine, writeToOutput) { + analytics.sendEvent(analytics.EVENT_CATEGORIES.APPLICATION, 'CliAutoComplete', this.configEnabled); + this.$textarea = $textarea; this.forceOpen = false; this.sendLine = sendLine; diff --git a/src/js/Features.js b/src/js/Features.js index 8d234075..40bca380 100644 --- a/src/js/Features.js +++ b/src/js/Features.js @@ -39,14 +39,23 @@ const Features = function (config) { self._features = features; self._featureMask = 0; + + self._analyticsChanges = {}; }; Features.prototype.getMask = function () { - return this._featureMask; + const self = this; + + analytics.sendChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self._analyticsChanges); + self._analyticsChanges = {}; + + return self._featureMask; }; Features.prototype.setMask = function (featureMask) { - this._featureMask = featureMask; + const self = this; + + self._featureMask = featureMask; }; Features.prototype.isEnabled = function (featureName) { @@ -159,25 +168,33 @@ Features.prototype.updateData = function (featureElement) { if (featureElement.attr('type') === 'checkbox') { const bit = featureElement.data('bit'); + let featureValue; if (featureElement.is(':checked')) { self._featureMask = bit_set(self._featureMask, bit); + featureValue = 'On'; } else { self._featureMask = bit_clear(self._featureMask, bit); + featureValue = 'Off'; } + self._analyticsChanges[`Feature${self.findFeatureByBit(bit).name}`] = featureValue; } else if (featureElement.prop('localName') === 'select') { const controlElements = featureElement.children(); const selectedBit = featureElement.val(); - if (selectedBit !== -1) { + let selectedFeature; for (const controlElement of controlElements) { const bit = controlElement.value; if (selectedBit === bit) { self._featureMask = bit_set(self._featureMask, bit); + selectedFeature = self.findFeatureByBit(bit); } else { self._featureMask = bit_clear(self._featureMask, bit); } } + if (selectedFeature) { + self._analyticsChanges[`FeatureGroup-${selectedFeature.group}`] = selectedFeature.name; + } } } }; diff --git a/src/js/backup_restore.js b/src/js/backup_restore.js index b7ea166c..bb405b13 100644 --- a/src/js/backup_restore.js +++ b/src/js/backup_restore.js @@ -225,6 +225,7 @@ function configuration_backup(callback) { return; } + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Backup'); console.log('Write SUCCESSFUL'); if (callback) callback(); }; @@ -307,6 +308,8 @@ function configuration_restore(callback) { configuration.FEATURE_CONFIG.features = features; } + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Restore'); + configuration_upload(configuration, callback); } else { GUI.log(i18n.getMessage('backupFileIncompatible')); diff --git a/src/js/main.js b/src/js/main.js index 9daf36e6..f0a4402d 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -67,13 +67,66 @@ function appReady() { }, }); - initializeSerialBackend(); + checkSetupAnalytics(function (analyticsService) { + analyticsService.sendEvent(analyticsService.EVENT_CATEGORIES.APPLICATION, 'SelectedLanguage', i18n.selectedLanguage); + }); - $('.connect_b a.connect').removeClass('disabled'); - $('.firmware_b a.flash').removeClass('disabled'); + initializeSerialBackend(); }); } +function checkSetupAnalytics(callback) { + if (!analytics) { + setTimeout(function () { + const result = ConfigStorage.get(['userId', 'analyticsOptOut', 'checkForConfiguratorUnstableVersions' ]); + if (!analytics) { + setupAnalytics(result); + } + + callback(analytics); + }); + } else if (callback) { + callback(analytics); + } +} + +function getBuildType() { + return GUI.Mode; +} + +function setupAnalytics(result) { + let userId; + if (result.userId) { + userId = result.userId; + } else { + const uid = new ShortUniqueId(); + userId = uid.randomUUID(13); + + ConfigStorage.set({ 'userId': userId }); + } + + const optOut = !!result.analyticsOptOut; + const checkForDebugVersions = !!result.checkForConfiguratorUnstableVersions; + + const debugMode = typeof process === "object" && process.versions['nw-flavor'] === 'sdk'; + + window.analytics = new Analytics('UA-123002063-1', userId, CONFIGURATOR.productName, CONFIGURATOR.version, CONFIGURATOR.gitRevision, GUI.operating_system, + checkForDebugVersions, optOut, debugMode, getBuildType()); + + function logException(exception) { + analytics.sendException(exception.stack); + } + + if (typeof process === "object") { + process.on('uncaughtException', logException); + } + + analytics.sendEvent(analytics.EVENT_CATEGORIES.APPLICATION, 'AppStart', { sessionControl: 'start' }); + + $('.connect_b a.connect').removeClass('disabled'); + $('.firmware_b a.flash').removeClass('disabled'); +} + function closeSerial() { // automatically close the port when application closes const connectionId = serial.connectionId; @@ -134,6 +187,8 @@ function closeHandler() { this.hide(); } + analytics.sendEvent(analytics.EVENT_CATEGORIES.APPLICATION, 'AppClose', { sessionControl: 'end' }); + closeSerial(); if (!GUI.isCordova()) { @@ -268,6 +323,10 @@ function startProcess() { GUI.tab_switch_in_progress = false; } + checkSetupAnalytics(function (analyticsService) { + analyticsService.sendAppView(tab); + }); + switch (tab) { case 'landing': import("./tabs/landing").then(({ landing }) => @@ -524,6 +583,9 @@ function startProcess() { $(expertModeCheckbox).on("change", () => { const checked = $(expertModeCheckbox).is(':checked'); + checkSetupAnalytics(function (analyticsService) { + analyticsService.setDimension(analyticsService.DIMENSIONS.CONFIGURATOR_EXPERT_MODE, checked ? 'On' : 'Off'); + }); if (FC.FEATURE_CONFIG && FC.FEATURE_CONFIG.features !== 0) { updateTabList(FC.FEATURE_CONFIG.features); @@ -567,6 +629,10 @@ function startProcess() { function setDarkTheme(enabled) { DarkTheme.setConfig(enabled); + + checkSetupAnalytics(function (analyticsService) { + analyticsService.sendEvent(analyticsService.EVENT_CATEGORIES.APPLICATION, 'DarkTheme', enabled); + }); } @@ -732,6 +798,8 @@ function showErrorDialog(message) { // TODO: all of these are used as globals in other parts. // once moved to modules extract to own module. +window.googleAnalytics = analytics; +window.analytics = null; window.showErrorDialog = showErrorDialog; window.generateFilename = generateFilename; window.updateTabList = updateTabList; @@ -739,3 +807,4 @@ window.isExpertModeEnabled = isExpertModeEnabled; window.checkForConfiguratorUpdates = checkForConfiguratorUpdates; window.setDarkTheme = setDarkTheme; window.appReady = appReady; +window.checkSetupAnalytics = checkSetupAnalytics; diff --git a/src/js/serial_backend.js b/src/js/serial_backend.js index 0ca16828..b64ccb5c 100644 --- a/src/js/serial_backend.js +++ b/src/js/serial_backend.js @@ -157,9 +157,16 @@ function finishClose(finishedCallback) { const wasConnected = CONFIGURATOR.connectionValid; + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Disconnected'); + if (connectionTimestamp) { + const connectedTime = Date.now() - connectionTimestamp; + analytics.sendTiming(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Connected', connectedTime); + } // close reset to custom defaults dialog $('#dialogResetToCustomDefaults')[0].close(); + analytics.resetFlightControllerData(); + serial.disconnect(onClosed); MSP.disconnect_cleanup(); @@ -242,13 +249,18 @@ function onOpen(openInfo) { console.log(`Requesting configuration data`); MSP.send_message(MSPCodes.MSP_API_VERSION, false, false, function () { + analytics.setFlightControllerData(analytics.DATA.API_VERSION, FC.CONFIG.apiVersion); + GUI.log(i18n.getMessage('apiVersionReceived', [FC.CONFIG.apiVersion])); if (semver.gte(FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_ACCEPTED)) { MSP.send_message(MSPCodes.MSP_FC_VARIANT, false, false, function () { + analytics.setFlightControllerData(analytics.DATA.FIRMWARE_TYPE, FC.CONFIG.flightControllerIdentifier); if (FC.CONFIG.flightControllerIdentifier === 'BTFL') { MSP.send_message(MSPCodes.MSP_FC_VERSION, false, false, function () { + analytics.setFlightControllerData(analytics.DATA.FIRMWARE_VERSION, FC.CONFIG.flightControllerVersion); + GUI.log(i18n.getMessage('fcInfoReceived', [FC.CONFIG.flightControllerIdentifier, FC.CONFIG.flightControllerVersion])); MSP.send_message(MSPCodes.MSP_BUILD_INFO, false, false, function () { @@ -259,6 +271,8 @@ function onOpen(openInfo) { }); }); } else { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'ConnectionRefusedFirmwareType', FC.CONFIG.flightControllerIdentifier); + const dialog = $('.dialogConnectWarning')[0]; $('.dialogConnectWarning-content').html(i18n.getMessage('firmwareTypeNotSupported')); @@ -273,6 +287,8 @@ function onOpen(openInfo) { } }); } else { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'ConnectionRefusedFirmwareVersion', FC.CONFIG.apiVersion); + const dialog = $('.dialogConnectWarning')[0]; $('.dialogConnectWarning-content').html(i18n.getMessage('firmwareVersionNotSupported', [CONFIGURATOR.API_VERSION_ACCEPTED])); @@ -287,6 +303,8 @@ function onOpen(openInfo) { } }); } else { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'SerialPortFailed'); + console.log('Failed to open serial port'); GUI.log(i18n.getMessage('serialPortOpenFail')); @@ -323,12 +341,20 @@ function abortConnect() { } function processBoardInfo() { + analytics.setFlightControllerData(analytics.DATA.BOARD_TYPE, FC.CONFIG.boardIdentifier); + analytics.setFlightControllerData(analytics.DATA.TARGET_NAME, FC.CONFIG.targetName); + analytics.setFlightControllerData(analytics.DATA.BOARD_NAME, FC.CONFIG.boardName); + analytics.setFlightControllerData(analytics.DATA.MANUFACTURER_ID, FC.CONFIG.manufacturerId); + analytics.setFlightControllerData(analytics.DATA.MCU_TYPE, FC.getMcuType()); + GUI.log(i18n.getMessage('boardInfoReceived', [FC.getHardwareName(), FC.CONFIG.boardVersion])); if (bit_check(FC.CONFIG.targetCapabilities, FC.TARGET_CAPABILITIES_FLAGS.SUPPORTS_CUSTOM_DEFAULTS) && bit_check(FC.CONFIG.targetCapabilities, FC.TARGET_CAPABILITIES_FLAGS.HAS_CUSTOM_DEFAULTS) && FC.CONFIG.configurationState === FC.CONFIGURATION_STATES.DEFAULTS_BARE) { const dialog = $('#dialogResetToCustomDefaults')[0]; $('#dialogResetToCustomDefaults-acceptbtn').click(function() { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'AcceptResetToCustomDefaults'); + const buffer = []; buffer.push(mspHelper.RESET_TYPES.CUSTOM_DEFAULTS); MSP.send_message(MSPCodes.MSP_RESET_CONF, buffer, false); @@ -341,6 +367,8 @@ function processBoardInfo() { }); $('#dialogResetToCustomDefaults-cancelbtn').click(function() { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'CancelResetToCustomDefaults'); + dialog.close(); setConnectionTimeout(); @@ -357,12 +385,15 @@ function processBoardInfo() { } function checkReportProblems() { + const PROBLEM_ANALYTICS_EVENT = 'ProblemFound'; const problemItemTemplate = $('#dialogReportProblems-listItemTemplate'); function checkReportProblem(problemName, problemDialogList) { if (bit_check(FC.CONFIG.configurationProblems, FC.CONFIGURATION_PROBLEM_FLAGS[problemName])) { problemItemTemplate.clone().html(i18n.getMessage(`reportProblemsDialog${problemName}`)).appendTo(problemDialogList); + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, PROBLEM_ANALYTICS_EVENT, problemName); + return true; } @@ -379,6 +410,9 @@ function checkReportProblems() { problemItemTemplate.clone().html(i18n.getMessage(`reportProblemsDialog${problemName}`, [CONFIGURATOR.latestVersion, CONFIGURATOR.latestVersionReleaseUrl, CONFIGURATOR.getDisplayVersion(), FC.CONFIG.flightControllerVersion])).appendTo(problemDialogList); needsProblemReportingDialog = true; + + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, PROBLEM_ANALYTICS_EVENT, + `${problemName};${CONFIGURATOR.API_VERSION_MAX_SUPPORTED};${FC.CONFIG.apiVersion}`); } needsProblemReportingDialog = checkReportProblem('MOTOR_PROTOCOL_DISABLED', problemDialogList) || needsProblemReportingDialog; @@ -406,6 +440,8 @@ function processUid() { MSP.send_message(MSPCodes.MSP_UID, false, false, function () { const deviceIdentifier = FC.CONFIG.deviceIdentifier; + analytics.setFlightControllerData(analytics.DATA.MCU_ID, objectHash.sha1(deviceIdentifier)); + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'Connected'); connectionTimestamp = Date.now(); GUI.log(i18n.getMessage('uniqueDeviceIdReceived', [deviceIdentifier])); diff --git a/src/js/tabs/cli.js b/src/js/tabs/cli.js index 4365d425..78f9461d 100644 --- a/src/js/tabs/cli.js +++ b/src/js/tabs/cli.js @@ -54,6 +54,7 @@ function getCliCommand(command, cliBuffer) { function copyToClipboard(text) { function onCopySuccessful() { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'CliCopyToClipboard', text.length); const button = TABS.cli.GUI.copyButton; const origText = button.text(); const origWidth = button.css("width"); @@ -176,6 +177,8 @@ cli.initialize = function (callback) { if (self.outputHistory.length > 0 && writer.length === 0) { writer.write(new Blob([self.outputHistory], {type: 'text/plain'})); } else { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'CliSave', self.outputHistory.length); + console.log('write complete'); } }; @@ -219,6 +222,8 @@ cli.initialize = function (callback) { function executeSnippet(fileName) { const commands = previewArea.val(); + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'CliExecuteFromFile', fileName); + executeCommands(commands); self.GUI.snippetPreviewWindow.close(); } diff --git a/src/js/tabs/configuration.js b/src/js/tabs/configuration.js index 1c31b3b5..867054f4 100644 --- a/src/js/tabs/configuration.js +++ b/src/js/tabs/configuration.js @@ -3,10 +3,12 @@ import { i18n } from '../localization'; import GUI from '../gui'; const configuration = { - // intended + analyticsChanges: {}, }; configuration.initialize = function (callback) { + const self = this; + if (GUI.active_tab != 'configuration') { GUI.active_tab = 'configuration'; GUI.configuration_loaded = true; @@ -48,6 +50,8 @@ configuration.initialize = function (callback) { load_serial_config(); function process_html() { + self.analyticsChanges = {}; + const features_e = $('.tab-configuration .features'); FC.FEATURE_CONFIG.features.generateElements(features_e); @@ -126,15 +130,39 @@ configuration.initialize = function (callback) { orientation_mag_e.val(FC.SENSOR_ALIGNMENT.align_mag); orientation_gyro_e.change(function () { - FC.SENSOR_ALIGNMENT.align_gyro = parseInt($(this).val()); + let value = parseInt($(this).val()); + + let newValue = undefined; + if (value !== FC.SENSOR_ALIGNMENT.align_gyro) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['GyroAlignment'] = newValue; + + FC.SENSOR_ALIGNMENT.align_gyro = value; }); orientation_acc_e.change(function () { - FC.SENSOR_ALIGNMENT.align_acc = parseInt($(this).val()); + let value = parseInt($(this).val()); + + let newValue = undefined; + if (value !== FC.SENSOR_ALIGNMENT.align_acc) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['AccAlignment'] = newValue; + + FC.SENSOR_ALIGNMENT.align_acc = value; }); orientation_mag_e.change(function () { - FC.SENSOR_ALIGNMENT.align_mag = parseInt($(this).val()); + let value = parseInt($(this).val()); + + let newValue = undefined; + if (value !== FC.SENSOR_ALIGNMENT.align_mag) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['MagAlignment'] = newValue; + + FC.SENSOR_ALIGNMENT.align_mag = value; }); // Multi gyro config @@ -178,11 +206,27 @@ configuration.initialize = function (callback) { $('.gyro_alignment_inputs_notfound').toggle(!detected_gyro_1 && !detected_gyro_2); orientation_gyro_1_align_e.change(function () { - FC.SENSOR_ALIGNMENT.gyro_1_align = parseInt($(this).val()); + let value = parseInt($(this).val()); + + let newValue = undefined; + if (value !== FC.SENSOR_ALIGNMENT.gyro_1_align) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['Gyro1Alignment'] = newValue; + + FC.SENSOR_ALIGNMENT.gyro_1_align = value; }); orientation_gyro_2_align_e.change(function () { - FC.SENSOR_ALIGNMENT.gyro_2_align = parseInt($(this).val()); + let value = parseInt($(this).val()); + + let newValue = undefined; + if (value !== FC.SENSOR_ALIGNMENT.gyro_2_align) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['Gyro2Alignment'] = newValue; + + FC.SENSOR_ALIGNMENT.gyro_2_align = value; }); // Gyro and PID update @@ -446,10 +490,20 @@ configuration.initialize = function (callback) { const value = parseInt(pidSelectElement.val()); + if (value !== FC.PID_ADVANCED_CONFIG.pid_process_denom) { + const newFrequency = pidSelectElement.find('option:selected').text(); + self.analyticsChanges['PIDLoopSettings'] = `denominator: ${value} | frequency: ${newFrequency}`; + } else { + self.analyticsChanges['PIDLoopSettings'] = undefined; + } + FC.PID_ADVANCED_CONFIG.pid_process_denom = value; FC.RX_CONFIG.fpvCamAngleDegrees = parseInt($('input[name="fpvCamAngleDegrees"]').val()); + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'configuration'); + self.analyticsChanges = {}; + // fill some data FC.GPS_CONFIG.auto_baud = $('input[name="gps_auto_baud"]').is(':checked') ? 1 : 0; FC.GPS_CONFIG.auto_config = $('input[name="gps_auto_config"]').is(':checked') ? 1 : 0; diff --git a/src/js/tabs/firmware_flasher.js b/src/js/tabs/firmware_flasher.js index f99e853f..d1d7a966 100644 --- a/src/js/tabs/firmware_flasher.js +++ b/src/js/tabs/firmware_flasher.js @@ -113,6 +113,7 @@ firmware_flasher.initialize = function (callback) { self.parsed_hex = data; if (self.parsed_hex) { + analytics.setFirmwareData(analytics.DATA.FIRMWARE_SIZE, self.parsed_hex.bytes_total); showLoadedHex(key); } else { self.flashingMessage(i18n.getMessage('firmwareFlasherHexCorrupted'), self.FLASH_MESSAGE_TYPES.INVALID); @@ -245,6 +246,8 @@ firmware_flasher.initialize = function (callback) { i18n.localizePage(); buildType_e.change(function() { + analytics.setFirmwareData(analytics.DATA.FIRMWARE_CHANNEL, $('option:selected', this).text()); + $("a.load_remote_file").addClass('disabled'); const build_type = $(this).val(); @@ -463,9 +466,13 @@ firmware_flasher.initialize = function (callback) { function flashFirmware(firmware) { const options = {}; + let eraseAll = false; if ($('input.erase_chip').is(':checked')) { options.erase_chip = true; + + eraseAll = true; } + analytics.setFirmwareData(analytics.DATA.FIRMWARE_ERASE_ALL, eraseAll.toString()); if (!$('option:selected', portPickerElement).data().isDFU) { if (String(portPickerElement.val()) !== '0') { @@ -482,12 +489,16 @@ firmware_flasher.initialize = function (callback) { baud = parseInt($('#flash_manual_baud_rate').val()); } + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'Flashing', self.fileName || null); + STM32.connect(port, baud, firmware, options); } else { console.log('Please select valid serial port'); GUI.log(i18n.getMessage('firmwareFlasherNoValidPort')); } } else { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'Flashing', self.fileName || null); + STM32DFU.connect(usbDevices, firmware, options); } } @@ -690,6 +701,9 @@ firmware_flasher.initialize = function (callback) { self.enableFlashing(false); self.developmentFirmwareLoaded = false; + analytics.setFirmwareData(analytics.DATA.FIRMWARE_CHANNEL, undefined); + analytics.setFirmwareData(analytics.DATA.FIRMWARE_SOURCE, 'file'); + chrome.fileSystem.chooseEntry({ type: 'openFile', accepts: [ @@ -711,6 +725,7 @@ firmware_flasher.initialize = function (callback) { console.log('Loading file from:', path); fileEntry.file(function (file) { + analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, file.name); const reader = new FileReader(); reader.onloadend = function(e) { @@ -724,6 +739,7 @@ firmware_flasher.initialize = function (callback) { self.parsed_hex = data; if (self.parsed_hex) { + analytics.setFirmwareData(analytics.DATA.FIRMWARE_SIZE, self.parsed_hex.bytes_total); self.localFirmwareLoaded = true; showLoadedHex(file.name); @@ -770,6 +786,8 @@ firmware_flasher.initialize = function (callback) { self.localFirmwareLoaded = false; self.developmentFirmwareLoaded = buildTypesToShow[$('select[name="build_type"]').val()].tag === 'firmwareFlasherOptionLabelBuildTypeDevelopment'; + analytics.setFirmwareData(analytics.DATA.FIRMWARE_SOURCE, 'http'); + if ($('select[name="firmware_version"]').val() === "0") { GUI.log(i18n.getMessage('firmwareFlasherNoFirmwareSelected')); return; @@ -856,7 +874,10 @@ firmware_flasher.initialize = function (callback) { return; } + analytics.setFirmwareData(analytics.DATA.FIRMWARE_NAME, response.file); + updateStatus('Pending', response.key, 0, false); + let retries = 1; self.releaseLoader.requestBuildStatus(response.key, (statusResponse) => { if (statusResponse.status !== "queued") { @@ -900,6 +921,7 @@ firmware_flasher.initialize = function (callback) { if (!exitDfuElement.hasClass('disabled')) { exitDfuElement.addClass("disabled"); if (!GUI.connect_lock) { // button disabled while flashing is in progress + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'ExitDfu', null); try { console.log('Closing DFU'); STM32DFU.connect(usbDevices, self.parsed_hex, { exitDfu: true }); @@ -1053,7 +1075,11 @@ firmware_flasher.initialize = function (callback) { // onwriteend will be fired again when truncation is finished truncated = true; writer.truncate(blob.size); + + return; } + + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLASHING, 'SaveFirmware', path); }; writer.write(blob); @@ -1131,6 +1157,8 @@ firmware_flasher.cleanup = function (callback) { $('div#flashbutton a.flash_state').removeClass('active'); $('div#flashbutton a.flash').removeClass('active'); + analytics.resetFirmwareData(); + if (callback) callback(); }; diff --git a/src/js/tabs/motors.js b/src/js/tabs/motors.js index 85fcca74..66740816 100644 --- a/src/js/tabs/motors.js +++ b/src/js/tabs/motors.js @@ -9,6 +9,7 @@ const motors = { previousDshotBidir: null, previousFilterDynQ: null, previousFilterDynCount: null, + analyticsChanges: {}, configHasChanged: false, configChanges: {}, feature3DEnabled: false, @@ -236,6 +237,7 @@ motors.initialize = async function (callback) { self.feature3DEnabled = FC.FEATURE_CONFIG.features.isEnabled('3D'); const motorsEnableTestModeElement = $('#motorsEnableTestMode'); + self.analyticsChanges = {}; motorsEnableTestModeElement.prop('checked', false).trigger('change'); @@ -357,8 +359,15 @@ motors.initialize = async function (callback) { reverseMotorSwitchElement.prop('checked', FC.MIXER_CONFIG.reverseMotorDir !== 0).change(); mixerListElement.change(function () { - FC.MIXER_CONFIG.mixer = parseInt($(this).val()); + const mixerValue = parseInt($(this).val()); + let newValue; + if (mixerValue !== FC.MIXER_CONFIG.mixer) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['Mixer'] = newValue; + + FC.MIXER_CONFIG.mixer = mixerValue; refreshMixerPreview(); }); @@ -679,7 +688,10 @@ motors.initialize = async function (callback) { self.previousFilterDynCount = FC.FILTER_CONFIG.dyn_notch_count; dshotBidirElement.on("change", function () { - FC.MOTOR_CONFIG.use_dshot_telemetry = dshotBidirElement.is(':checked'); + const value = dshotBidirElement.is(':checked'); + const newValue = (value !== FC.MOTOR_CONFIG.use_dshot_telemetry) ? 'On' : 'Off'; + self.analyticsChanges['BidirectionalDshot'] = newValue; + FC.MOTOR_CONFIG.use_dshot_telemetry = value; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { const rpmFilterIsDisabled = FC.FILTER_CONFIG.gyro_rpm_notch_harmonics === 0; @@ -773,7 +785,17 @@ motors.initialize = async function (callback) { escProtocolElement.val(FC.PID_ADVANCED_CONFIG.fast_pwm_protocol + 1); - escProtocolElement.on("change", () => updateVisibility()).trigger("change"); + escProtocolElement.on("change", function () { + const escProtocolValue = parseInt($(this).val()) - 1; + + let newValue = undefined; + if (escProtocolValue !== FC.PID_ADVANCED_CONFIG.fast_pwm_protocol) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['EscProtocol'] = newValue; + + updateVisibility(); + }).trigger("change"); //trigger change dshotBidir and ESC_SENSOR to show/hide Motor Poles tab dshotBidirElement.change(updateVisibility).trigger("change"); @@ -1120,6 +1142,8 @@ motors.initialize = async function (callback) { } await MSP.promise(MSPCodes.MSP_EEPROM_WRITE); + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'motors'); + self.analyticsChanges = {}; self.configHasChanged = false; reboot(); diff --git a/src/js/tabs/onboard_logging.js b/src/js/tabs/onboard_logging.js index 22f3b6f3..1cf54ac6 100644 --- a/src/js/tabs/onboard_logging.js +++ b/src/js/tabs/onboard_logging.js @@ -137,6 +137,8 @@ onboard_logging.initialize = function (callback) { .toggleClass("msc-supported", true); $('a.onboardLoggingRebootMsc').click(function () { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'RebootMsc'); + const buffer = []; if (GUI.operating_system === "Linux") { // Reboot into MSC using UTC time offset instead of user timezone @@ -404,26 +406,38 @@ onboard_logging.initialize = function (callback) { $('a.onboardLoggingRebootMsc').removeClass('disabled'); } + let loggingStatus; switch (FC.SDCARD.state) { case MSP.SDCARD_STATE_NOT_PRESENT: $(".sdcard-status").text(i18n.getMessage('sdcardStatusNoCard')); - break; + loggingStatus = 'SdCard: NotPresent'; + break; case MSP.SDCARD_STATE_FATAL: $(".sdcard-status").html(i18n.getMessage('sdcardStatusReboot')); - break; + loggingStatus = 'SdCard: Error'; + break; case MSP.SDCARD_STATE_READY: $(".sdcard-status").text(i18n.getMessage('sdcardStatusReady')); - break; + loggingStatus = 'SdCard: Ready'; + break; case MSP.SDCARD_STATE_CARD_INIT: $(".sdcard-status").text(i18n.getMessage('sdcardStatusStarting')); - break; + loggingStatus = 'SdCard: Init'; + break; case MSP.SDCARD_STATE_FS_INIT: $(".sdcard-status").text(i18n.getMessage('sdcardStatusFileSystem')); - break; + loggingStatus = 'SdCard: FsInit'; + break; default: $(".sdcard-status").text(i18n.getMessage('sdcardStatusUnknown',[FC.SDCARD.state])); } + if (dataflashPresent && FC.SDCARD.state === MSP.SDCARD_STATE_NOT_PRESENT) { + loggingStatus = 'Dataflash'; + analytics.setFlightControllerData(analytics.DATA.LOG_SIZE, FC.DATAFLASH.usedSize); + } + analytics.setFlightControllerData(analytics.DATA.LOGGING_STATUS, loggingStatus); + if (FC.SDCARD.supported && !sdcardTimer) { // Poll for changes in SD card status sdcardTimer = setTimeout(function() { @@ -455,8 +469,9 @@ onboard_logging.initialize = function (callback) { } function mark_saving_dialog_done(startTime, totalBytes, totalBytesCompressed) { - const totalTime = (new Date().getTime() - startTime) / 1000; + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'SaveDataflash'); + const totalTime = (new Date().getTime() - startTime) / 1000; console.log(`Received ${totalBytes} bytes in ${totalTime.toFixed(2)}s (${ (totalBytes / totalTime / 1024).toFixed(2)}kB / s) with block size ${self.blockSize}.`); if (!isNaN(totalBytesCompressed)) { @@ -616,6 +631,9 @@ onboard_logging.initialize = function (callback) { }; onboard_logging.cleanup = function (callback) { + analytics.setFlightControllerData(analytics.DATA.LOGGING_STATUS, undefined); + analytics.setFlightControllerData(analytics.DATA.LOG_SIZE, undefined); + if (sdcardTimer) { clearTimeout(sdcardTimer); sdcardTimer = false; diff --git a/src/js/tabs/options.js b/src/js/tabs/options.js index f58b9a1b..fd2e334b 100644 --- a/src/js/tabs/options.js +++ b/src/js/tabs/options.js @@ -14,6 +14,7 @@ options.initialize = function (callback) { TABS.options.initPermanentExpertMode(); TABS.options.initRememberLastTab(); TABS.options.initCheckForConfiguratorUnstableVersions(); + TABS.options.initAnalyticsOptOut(); TABS.options.initCliAutoComplete(); TABS.options.initShowAllSerialDevices(); TABS.options.initShowVirtualMode(); @@ -82,6 +83,31 @@ options.initCheckForConfiguratorUnstableVersions = function () { }); }; +options.initAnalyticsOptOut = function () { + const result = ConfigStorage.get('analyticsOptOut'); + if (result.analyticsOptOut) { + $('div.analyticsOptOut input').prop('checked', true); + } + + $('div.analyticsOptOut input').change(function () { + const checked = $(this).is(':checked'); + + ConfigStorage.set({'analyticsOptOut': checked}); + + checkSetupAnalytics(function (analyticsService) { + if (checked) { + analyticsService.sendEvent(analyticsService.EVENT_CATEGORIES.APPLICATION, 'OptOut'); + } + + analyticsService.setOptOut(checked); + + if (!checked) { + analyticsService.sendEvent(analyticsService.EVENT_CATEGORIES.APPLICATION, 'OptIn'); + } + }); + }).change(); +}; + options.initCliAutoComplete = function () { $('div.cliAutoComplete input') .prop('checked', CliAutoComplete.configEnabled) diff --git a/src/js/tabs/osd.js b/src/js/tabs/osd.js index db5a2384..9ae2db5d 100644 --- a/src/js/tabs/osd.js +++ b/src/js/tabs/osd.js @@ -2561,7 +2561,7 @@ OSD.GUI.preview = { }; const osd = { - // intentional + analyticsChanges: {}, }; osd.initialize = function(callback) { @@ -2839,6 +2839,11 @@ osd.initialize = function(callback) { fieldChanged.enabled = !fieldChanged.enabled; + if (self.analyticsChanges[`OSDStatistic${fieldChanged.name}`] === undefined) { + self.analyticsChanges[`OSDStatistic${fieldChanged.name}`] = 0; + } + self.analyticsChanges[`OSDStatistic${fieldChanged.name}`] += fieldChanged.enabled ? 1 : -1; + MSP.promise(MSPCodes.MSP_SET_OSD_CONFIG, OSD.msp.encodeStatistics(fieldChanged)) .then(updateOsdView); }), @@ -2876,6 +2881,11 @@ osd.initialize = function(callback) { const fieldChanged = $(this).data('field'); fieldChanged.enabled = !fieldChanged.enabled; + if (self.analyticsChanges[`OSDWarning${fieldChanged.name}`] === undefined) { + self.analyticsChanges[`OSDWarning${fieldChanged.name}`] = 0; + } + self.analyticsChanges[`OSDWarning${fieldChanged.name}`] += fieldChanged.enabled ? 1 : -1; + MSP.promise(MSPCodes.MSP_SET_OSD_CONFIG, OSD.msp.encodeOther()) .then(updateOsdView); }), @@ -2993,6 +3003,11 @@ osd.initialize = function(callback) { const $position = $(this).parent().find(`.position.${fieldChanged.name}`); fieldChanged.isVisible[profile] = !fieldChanged.isVisible[profile]; + if (self.analyticsChanges[`OSDElement${fieldChanged.name}`] === undefined) { + self.analyticsChanges[`OSDElement${fieldChanged.name}`] = 0; + } + self.analyticsChanges[`OSDElement${fieldChanged.name}`] += fieldChanged.isVisible[profile] ? 1 : -1; + if (fieldChanged.isVisible[OSD.getCurrentPreviewProfile()]) { $position.show(); } else { @@ -3232,6 +3247,20 @@ osd.initialize = function(callback) { setTimeout(() => { $(this).html(oldText); }, 1500); + + Object.keys(self.analyticsChanges).forEach(function(change) { + const value = self.analyticsChanges[change]; + if (value > 0) { + self.analyticsChanges[change] = 'On'; + } else if (value < 0) { + self.analyticsChanges[change] = 'Off'; + } else { + self.analyticsChanges[change] = undefined; + } + }); + + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'osd'); + self.analyticsChanges = {}; }); // font preview window @@ -3346,6 +3375,8 @@ osd.initialize = function(callback) { } }); + self.analyticsChanges = {}; + MSP.promise(MSPCodes.MSP_RX_CONFIG) .finally(() => { GUI.content_ready(callback); diff --git a/src/js/tabs/pid_tuning.js b/src/js/tabs/pid_tuning.js index c2adaf81..1425ad70 100644 --- a/src/js/tabs/pid_tuning.js +++ b/src/js/tabs/pid_tuning.js @@ -17,6 +17,7 @@ const pid_tuning = { SETPOINT_WEIGHT_RANGE_HIGH: 20, SETPOINT_WEIGHT_RANGE_LEGACY: 2.54, activeSubtab: 'pid', + analyticsChanges: {}, CONFIGURATOR_PIDS: [], CONFIGURATOR_ADVANCED_TUNING: {}, @@ -1111,7 +1112,15 @@ pid_tuning.initialize = function (callback) { FC.FILTER_CONFIG.yaw_lowpass_hz = parseInt($('.pid_filter input[name="yawLowpassFrequency"]').val()); if (vbatpidcompensationIsUsed) { - FC.ADVANCED_TUNING.vbatPidCompensation = $('input[id="vbatpidcompensation"]').is(':checked') ? 1 : 0; + const element = $('input[id="vbatpidcompensation"]'); + const value = element.is(':checked') ? 1 : 0; + let analyticsValue = undefined; + if (value !== FC.ADVANCED_TUNING.vbatPidCompensation) { + analyticsValue = element.is(':checked'); + } + self.analyticsChanges['VbatPidCompensation'] = analyticsValue; + + FC.ADVANCED_TUNING.vbatPidCompensation = value; } FC.ADVANCED_TUNING.deltaMethod = $('#pid-tuning .delta select').val(); @@ -1206,7 +1215,15 @@ pid_tuning.initialize = function (callback) { FC.ADVANCED_TUNING.motorOutputLimit = parseInt($('.tab-pid_tuning input[name="motorLimit"]').val()); FC.ADVANCED_TUNING.autoProfileCellCount = parseInt($('.tab-pid_tuning select[name="cellCount"]').val()); FC.ADVANCED_TUNING.idleMinRpm = parseInt($('input[name="idleMinRpm-number"]').val()); - FC.RC_TUNING.rates_type = $('select[id="ratesType"]').val(); + + const selectedRatesType = $('select[id="ratesType"]').val(); // send analytics for rates type + let selectedRatesTypeName = undefined; + if (selectedRatesType !== FC.RC_TUNING.rates_type) { + selectedRatesTypeName = $('select[id="ratesType"]').find('option:selected').text(); + } + self.analyticsChanges['RatesType'] = selectedRatesTypeName; + + FC.RC_TUNING.rates_type = selectedRatesType; } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { @@ -1993,6 +2010,7 @@ pid_tuning.initialize = function (callback) { } self.calculateNewPids(); + self.analyticsChanges['PidTuningSliders'] = "On"; }); // reset to middle with double click @@ -2084,9 +2102,11 @@ pid_tuning.initialize = function (callback) { if (slider.is('#sliderGyroFilterMultiplier')) { TuningSliders.sliderGyroFilterMultiplier = sliderValue; self.calculateNewGyroFilters(); + self.analyticsChanges['GyroFilterTuningSlider'] = "On"; } else if (slider.is('#sliderDTermFilterMultiplier')) { TuningSliders.sliderDTermFilterMultiplier = sliderValue; self.calculateNewDTermFilters(); + self.analyticsChanges['DTermFilterTuningSlider'] = "On"; } }); @@ -2115,6 +2135,13 @@ pid_tuning.initialize = function (callback) { } else { TuningSliders.updateFilterSlidersDisplay(); } + + if (TuningSliders.GyroSliderUnavailable) { + self.analyticsChanges['GyroFilterTuningSlider'] = "Off"; + } + if (TuningSliders.DTermSliderUnavailable) { + self.analyticsChanges['DTermFilterTuningSlider'] = "Off"; + } }); // update on filter switch changes @@ -2161,6 +2188,8 @@ pid_tuning.initialize = function (callback) { if ($('input[id="useIntegratedYaw"]').is(':checked')) { $('input[id="useIntegratedYaw"]').prop('checked', true).click(); } + + self.analyticsChanges['PidTuningSliders'] = "On"; }); // enable Filter sliders button (legacy sliders) @@ -2171,18 +2200,23 @@ pid_tuning.initialize = function (callback) { $('input[id="gyroLowpassEnabled"]').prop('checked', true).click(); $('input[id="gyroLowpass2Enabled"]').prop('checked', false).click(); TuningSliders.resetGyroFilterSlider(); + + self.analyticsChanges['GyroFilterTuningSlider'] = "On"; } if (TuningSliders.DTermSliderUnavailable) { $('input[id="dtermLowpassDynEnabled"]').prop('checked', false).click(); $('input[id="dtermLowpassEnabled"]').prop('checked', true).click(); $('input[id="dtermLowpass2Enabled"]').prop('checked', false).click(); TuningSliders.resetDTermFilterSlider(); + + self.analyticsChanges['DTermFilterTuningSlider'] = "On"; } }); // update on pid table inputs $('#pid_main input').on('input', function() { TuningSliders.updatePidSlidersDisplay(); + self.analyticsChanges['PidTuningSliders'] = "Off"; }); } @@ -2228,6 +2262,9 @@ pid_tuning.initialize = function (callback) { self.refresh(); }); + + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'pid_tuning'); + self.analyticsChanges = {}; }); // Setup model for rates preview @@ -2244,6 +2281,8 @@ pid_tuning.initialize = function (callback) { MSP.send_message(MSPCodes.MSP_STATUS_EX, false, false, self.checkUpdateProfile(true)); }, 500, true); + self.analyticsChanges = {}; + GUI.content_ready(callback); TABS.pid_tuning.isHtmlProcessing = false; } diff --git a/src/js/tabs/ports.js b/src/js/tabs/ports.js index 87fb089f..4900407c 100644 --- a/src/js/tabs/ports.js +++ b/src/js/tabs/ports.js @@ -3,10 +3,12 @@ import { i18n } from "../localization"; import GUI from '../gui'; const ports = { - // intentional + analyticsChanges: {}, }; ports.initialize = function (callback) { + const self = this; + let board_definition = {}; const functionRules = [ @@ -118,6 +120,8 @@ ports.initialize = function (callback) { } function update_ui() { + self.analyticsChanges = {}; + $(".tab-ports").addClass("supported"); const VCP_PORT_IDENTIFIER = 20; @@ -258,6 +262,19 @@ ports.initialize = function (callback) { lastMspSelected = functionName; } } + + if (column === 'telemetry') { + const initialValue = functionName; + selectElement.on('change', function () { + const telemetryValue = $(this).val(); + + let newValue; + if (telemetryValue !== initialValue) { + newValue = $(this).find('option:selected').text(); + } + self.analyticsChanges['Telemetry'] = newValue; + }); + } } } } @@ -303,10 +320,14 @@ ports.initialize = function (callback) { }); if (lastVtxControlSelected !== vtxControlSelected) { + self.analyticsChanges['VtxControl'] = vtxControlSelected; + lastVtxControlSelected = vtxControlSelected; } if (lastMspSelected !== mspControlSelected) { + self.analyticsChanges['MspControl'] = mspControlSelected; + lastMspSelected = mspControlSelected; } @@ -335,6 +356,9 @@ ports.initialize = function (callback) { } function on_save_handler() { + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'ports'); + self.analyticsChanges = {}; + // update configuration based on current ui state FC.SERIAL_CONFIG.ports = []; diff --git a/src/js/tabs/power.js b/src/js/tabs/power.js index bd2b9d36..b568fd5d 100644 --- a/src/js/tabs/power.js +++ b/src/js/tabs/power.js @@ -3,10 +3,17 @@ import GUI from '../gui'; const power = { supported: false, + analyticsChanges: {}, }; power.initialize = function (callback) { - GUI.active_tab = 'power'; + const self = this; + + if (GUI.active_tab != 'power') { + GUI.active_tab = 'power'; + // Disabled on merge into betaflight-configurator + //googleAnalytics.sendAppView('Power'); + } if (GUI.calibrationManager) { GUI.calibrationManager.destroy(); @@ -105,6 +112,14 @@ power.initialize = function (callback) { $(`input[name="vbatresdivmultiplier-${index}"]`).val(voltageDataSource[index].vbatresdivmultiplier); } + $('input[name="vbatscale-0"]').change(function () { + const value = parseInt($(this).val()); + + if (value !== voltageDataSource[0].vbatscale) { + self.analyticsChanges['PowerVBatUpdated'] = value; + } + }); + // amperage meters if (FC.BATTERY_CONFIG.currentMeterSource == 0) { $('.boxAmperageConfiguration').hide(); @@ -155,6 +170,26 @@ power.initialize = function (callback) { $(`input[name="amperageoffset-${index}"]`).val(currentDataSource[index].offset); } + $('input[name="amperagescale-0"]').change(function () { + if (FC.BATTERY_CONFIG.currentMeterSource === 1) { + let value = parseInt($(this).val()); + + if (value !== currentDataSource[0].scale) { + self.analyticsChanges['PowerAmperageUpdated'] = value; + } + } + }); + + $('input[name="amperagescale-1"]').change(function () { + if (FC.BATTERY_CONFIG.currentMeterSource === 2) { + let value = parseInt($(this).val()); + + if (value !== currentDataSource[1].scale) { + self.analyticsChanges['PowerAmperageUpdated'] = value; + } + } + }); + if(FC.BATTERY_CONFIG.voltageMeterSource == 1 || FC.BATTERY_CONFIG.currentMeterSource == 1 || FC.BATTERY_CONFIG.currentMeterSource == 2) { $('.calibration').show(); } else { @@ -399,6 +434,14 @@ power.initialize = function (callback) { $('output[name="amperagenewscale"').val(amperagenewscale); $('a.applycalibration').click(function() { + if (vbatscalechanged) { + self.analyticsChanges['PowerVBatUpdated'] = 'Calibrated'; + } + + if (amperagescalechanged) { + self.analyticsChanges['PowerAmperageUpdated'] = 'Calibrated'; + } + calibrationconfirmed = true; GUI.calibrationManagerConfirmation.close(); updateDisplay(FC.VOLTAGE_METER_CONFIGS, FC.CURRENT_METER_CONFIGS); @@ -430,6 +473,8 @@ power.initialize = function (callback) { FC.BATTERY_CONFIG.vbatwarningcellvoltage = parseFloat($('input[name="warningcellvoltage"]').val()); FC.BATTERY_CONFIG.capacity = parseInt($('input[name="capacity"]').val()); + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'power'); + save_power_config(); }); @@ -463,6 +508,7 @@ power.initialize = function (callback) { } function process_html() { + self.analyticsChanges = {}; // translate to user-selected language i18n.localizePage(); diff --git a/src/js/tabs/receiver.js b/src/js/tabs/receiver.js index 6b403d08..825434b6 100644 --- a/src/js/tabs/receiver.js +++ b/src/js/tabs/receiver.js @@ -6,6 +6,7 @@ import CryptoES from 'crypto-es'; const receiver = { rateChartHeight: 117, + analyticsChanges: {}, needReboot: false, elrsPassphraseEnabled: false, }; @@ -75,6 +76,8 @@ receiver.initialize = function (callback) { MSP.send_message(MSPCodes.MSP_FEATURE_CONFIG, false, false, get_rc_data); function process_html() { + self.analyticsChanges = {}; + const featuresElement = $('.tab-receiver .features'); FC.FEATURE_CONFIG.features.generateElements(featuresElement); @@ -240,9 +243,12 @@ receiver.initialize = function (callback) { serialRxSelectElement.change(function () { const serialRxValue = parseInt($(this).val()); + let newValue; if (serialRxValue !== FC.RX_CONFIG.serialrx_provider) { + newValue = $(this).find('option:selected').text(); updateSaveButton(true); } + tab.analyticsChanges['SerialRx'] = newValue; FC.RX_CONFIG.serialrx_provider = serialRxValue; }); @@ -296,9 +302,12 @@ receiver.initialize = function (callback) { spiRxElement.change(function () { const value = parseInt($(this).val()); + let newValue = undefined; if (value !== FC.RX_CONFIG.rxSpiProtocol) { + newValue = $(this).find('option:selected').text(); updateSaveButton(true); } + tab.analyticsChanges['SPIRXProtocol'] = newValue; FC.RX_CONFIG.rxSpiProtocol = value; }); @@ -484,6 +493,9 @@ receiver.initialize = function (callback) { } } + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, tab.analyticsChanges, 'receiver'); + tab.analyticsChanges = {}; + MSP.send_message(MSPCodes.MSP_SET_RX_MAP, mspHelper.crunch(MSPCodes.MSP_SET_RX_MAP), false, save_rssi_config); } diff --git a/src/js/tabs/setup.js b/src/js/tabs/setup.js index dab440d3..8230c2c0 100644 --- a/src/js/tabs/setup.js +++ b/src/js/tabs/setup.js @@ -8,7 +8,9 @@ const setup = { setup.initialize = function (callback) { const self = this; - GUI.active_tab = 'setup'; + if (GUI.active_tab != 'setup') { + GUI.active_tab = 'setup'; + } function load_status() { MSP.send_message(MSPCodes.MSP_STATUS, false, false, load_mixer_config); diff --git a/src/js/tabs/setup_osd.js b/src/js/tabs/setup_osd.js index 409ffb34..d7b4a98c 100644 --- a/src/js/tabs/setup_osd.js +++ b/src/js/tabs/setup_osd.js @@ -6,7 +6,11 @@ const setup_osd = { setup_osd.initialize = function (callback) { - GUI.active_tab = 'setup_osd'; + if (GUI.active_tab != 'setup_osd') { + GUI.active_tab = 'setup_osd'; + // Disabled on merge into betaflight-configurator + //googleAnalytics.sendAppView('Setup OSD'); + } function load_status() { MSP.send_message(MSPCodes.MSP_STATUS, false, false, load_html); diff --git a/src/js/tabs/transponder.js b/src/js/tabs/transponder.js index 9452953b..339826fc 100644 --- a/src/js/tabs/transponder.js +++ b/src/js/tabs/transponder.js @@ -110,6 +110,8 @@ transponder.initialize = function(callback) { ///////////////////////////////////////////// GUI.active_tab = 'transponder'; + // Disabled on merge into betaflight-configurator + //googleAnalytics.sendAppView('Transponder'); // transponder supported added in MSP API Version 1.16.0 if (FC.CONFIG) { diff --git a/src/js/tabs/vtx.js b/src/js/tabs/vtx.js index bb1476af..0f7fa910 100644 --- a/src/js/tabs/vtx.js +++ b/src/js/tabs/vtx.js @@ -11,6 +11,7 @@ const vtx = { MAX_BAND_CHANNELS_VALUES: 8, VTXTABLE_BAND_LIST: [], VTXTABLE_POWERLEVEL_LIST: [], + analyticsChanges: {}, updating: true, env: new djv(), get _DEVICE_STATUS_UPDATE_INTERVAL_NAME() { @@ -55,7 +56,9 @@ vtx.initialize = function (callback) { GUI.active_tab = 'vtx'; } - self.supported = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42); + self.analyticsChanges = {}; + + this.supported = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42); if (!this.supported) { load_html(); @@ -401,6 +404,13 @@ vtx.initialize = function (callback) { } $("#vtx_table_channels").on('input', showHideBandChannels).trigger('input'); + $("#vtx_table").change(function() { + let fromScratch = true; + if (self.analyticsChanges['VtxTableLoadFromClipboard'] !== undefined || self.analyticsChanges['VtxTableLoadFromFile'] !== undefined) { + fromScratch = false; + } + self.analyticsChanges['VtxTableEdit'] = fromScratch ? 'modificationOnly' : 'fromTemplate'; + }); /*** Helper functions */ @@ -636,6 +646,7 @@ vtx.initialize = function (callback) { // we get here at the end of the truncate method, change to the new end writer.onwriteend = function() { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'VtxTableLuaSave', text.length); console.log('Write VTX table lua file end'); GUI.log(i18n.getMessage('vtxSavedLuaFileOk')); }; @@ -686,6 +697,7 @@ vtx.initialize = function (callback) { // we get here at the end of the truncate method, change to the new end writer.onwriteend = function() { + analytics.sendEvent(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, 'VtxTableSave', text.length); console.log(vtxConfig); console.log('Write VTX file end'); GUI.log(i18n.getMessage('vtxSavedFileOk')); @@ -736,6 +748,9 @@ vtx.initialize = function (callback) { TABS.vtx.vtxTableSavePending = true; + self.analyticsChanges['VtxTableLoadFromClipboard'] = undefined; + self.analyticsChanges['VtxTableLoadFromFile'] = file.name; + console.log('Load VTX file end'); GUI.log(i18n.getMessage('vtxLoadFileOk')); }, @@ -783,6 +798,9 @@ vtx.initialize = function (callback) { TABS.vtx.vtxTableSavePending = true; + self.analyticsChanges['VtxTableLoadFromFile'] = undefined; + self.analyticsChanges['VtxTableLoadFromClipboard'] = text.length; + console.log('Load VTX clipboard end'); GUI.log(i18n.getMessage('vtxLoadClipboardOk')); }, @@ -816,6 +834,8 @@ vtx.initialize = function (callback) { // Start MSP saving save_vtx_config(); + analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'vtx'); + function save_vtx_config() { MSP.send_message(MSPCodes.MSP_SET_VTX_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_VTX_CONFIG), false, save_vtx_powerlevels); } diff --git a/src/main.html b/src/main.html index ce3edb70..d963bd79 100644 --- a/src/main.html +++ b/src/main.html @@ -60,6 +60,7 @@ + @@ -77,6 +78,7 @@ + @@ -106,6 +108,7 @@ + diff --git a/src/tabs/landing.html b/src/tabs/landing.html index 1fa5bacb..498348c3 100644 --- a/src/tabs/landing.html +++ b/src/tabs/landing.html @@ -45,7 +45,10 @@
- + +
+
+
diff --git a/src/tabs/options.html b/src/tabs/options.html index 31b0feaa..b00548ea 100644 --- a/src/tabs/options.html +++ b/src/tabs/options.html @@ -23,6 +23,12 @@ +
+
+ +
+ +