src/extensions/default/HealthData/HealthDataManager.js
/*
* Copyright (c) 2015 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*global define, $, brackets, console, appshell */
define(function (require, exports, module) {
"use strict";
var AppInit = brackets.getModule("utils/AppInit"),
CommandManager = brackets.getModule("command/CommandManager"),
HealthLogger = brackets.getModule("utils/HealthLogger"),
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
UrlParams = brackets.getModule("utils/UrlParams").UrlParams,
Strings = brackets.getModule("strings"),
HealthDataUtils = require("HealthDataUtils"),
uuid = require("thirdparty/uuid"),
prefs = PreferencesManager.getExtensionPrefs("healthData"),
params = new UrlParams(),
ONE_MINUTE = 60 * 1000,
ONE_DAY = 24 * 60 * ONE_MINUTE,
FIRST_LAUNCH_SEND_DELAY = 30 * ONE_MINUTE,
timeoutVar;
prefs.definePreference("healthDataTracking", "boolean", true, {
description: Strings.DESCRIPTION_HEALTH_DATA_TRACKING
});
params.parse();
/**
* Get the Health Data which will be sent to the server. Initially it is only one time data.
*/
function getHealthData() {
var result = new $.Deferred(),
oneTimeHealthData = {};
oneTimeHealthData.snapshotTime = Date.now();
oneTimeHealthData.os = brackets.platform;
oneTimeHealthData.userAgent = window.navigator.userAgent;
oneTimeHealthData.osLanguage = brackets.app.language;
oneTimeHealthData.bracketsLanguage = brackets.getLocale();
oneTimeHealthData.bracketsVersion = brackets.metadata.version;
$.extend(oneTimeHealthData, HealthLogger.getAggregatedHealthData());
HealthDataUtils.getUserInstalledExtensions()
.done(function (userInstalledExtensions) {
oneTimeHealthData.installedExtensions = userInstalledExtensions;
})
.always(function () {
HealthDataUtils.getUserInstalledTheme()
.done(function (bracketsTheme) {
oneTimeHealthData.bracketsTheme = bracketsTheme;
})
.always(function () {
var userUuid = PreferencesManager.getViewState("UUID");
var olderUuid = PreferencesManager.getViewState("OlderUUID");
if (userUuid && olderUuid) {
oneTimeHealthData.uuid = userUuid;
oneTimeHealthData.olderuuid = olderUuid;
return result.resolve(oneTimeHealthData);
} else {
// So we are going to get the Machine hash in either of the cases.
if (appshell.app.getMachineHash) {
appshell.app.getMachineHash(function (err, macHash) {
var generatedUuid;
if (err) {
generatedUuid = uuid.v4();
} else {
generatedUuid = macHash;
}
if (!userUuid) {
// Could be a new user. In this case
// both will remain the same.
userUuid = olderUuid = generatedUuid;
} else {
// For existing user, we will still cache
// the older uuid, so that we can improve
// our reporting in terms of figuring out
// the new users accurately.
olderUuid = userUuid;
userUuid = generatedUuid;
}
PreferencesManager.setViewState("UUID", userUuid);
PreferencesManager.setViewState("OlderUUID", olderUuid);
oneTimeHealthData.uuid = userUuid;
oneTimeHealthData.olderuuid = olderUuid;
return result.resolve(oneTimeHealthData);
});
} else {
// Probably running on older shell, in which case we will
// assign the same uuid to olderuuid.
if (!userUuid) {
oneTimeHealthData.uuid = oneTimeHealthData.olderuuid = uuid.v4();
} else {
oneTimeHealthData.olderuuid = userUuid;
}
PreferencesManager.setViewState("UUID", oneTimeHealthData.uuid);
PreferencesManager.setViewState("OlderUUID", oneTimeHealthData.olderuuid);
return result.resolve(oneTimeHealthData);
}
}
});
});
return result.promise();
}
/**
*@param{Object} eventParams contails Event Data
* will return complete Analyics Data in Json Format
*/
function getAnalyticsData(eventParams) {
var userUuid = PreferencesManager.getViewState("UUID"),
olderUuid = PreferencesManager.getViewState("OlderUUID");
//Create default Values
var defaultEventParams = {
eventCategory: "pingData",
eventSubCategory: "",
eventType: "",
eventSubType: ""
};
//Override with default values if not present
if (!eventParams) {
eventParams = defaultEventParams;
} else {
var e;
for (e in defaultEventParams) {
if (defaultEventParams.hasOwnProperty(e) && !eventParams[e]) {
eventParams[e] = defaultEventParams[e];
}
}
}
return {
project: brackets.config.serviceKey,
environment: brackets.config.environment,
time: new Date().toISOString(),
ingesttype: "dunamis",
data: {
"event.guid": uuid.v4(),
"event.user_guid": olderUuid || userUuid,
"event.dts_end": new Date().toISOString(),
"event.category": eventParams.eventCategory,
"event.subcategory": eventParams.eventSubCategory,
"event.type": eventParams.eventType,
"event.subtype": eventParams.eventSubType,
"event.user_agent": window.navigator.userAgent || "",
"event.language": brackets.app.language,
"source.name": brackets.metadata.version,
"source.platform": brackets.platform,
"source.version": brackets.metadata.version
}
};
}
/**
* Send data to the server
*/
function sendHealthDataToServer() {
var result = new $.Deferred();
getHealthData().done(function (healthData) {
var url = brackets.config.healthDataServerURL,
data = JSON.stringify(healthData);
$.ajax({
url: url,
type: "POST",
data: data,
dataType: "text",
contentType: "text/plain"
})
.done(function () {
result.resolve();
})
.fail(function (jqXHR, status, errorThrown) {
console.error("Error in sending Health Data. Response : " + jqXHR.responseText + ". Status : " + status + ". Error : " + errorThrown);
result.reject();
});
})
.fail(function () {
result.reject();
});
return result.promise();
}
// Send Analytics data to Server
function sendAnalyticsDataToServer(eventParams) {
var result = new $.Deferred();
var analyticsData = getAnalyticsData(eventParams);
$.ajax({
url: brackets.config.analyticsDataServerURL,
type: "POST",
data: JSON.stringify({events: [analyticsData]}),
headers: {
"Content-Type": "application/json",
"x-api-key": brackets.config.serviceKey
}
})
.done(function () {
result.resolve();
})
.fail(function (jqXHR, status, errorThrown) {
console.error("Error in sending Adobe Analytics Data. Response : " + jqXHR.responseText + ". Status : " + status + ". Error : " + errorThrown);
result.reject();
});
return result.promise();
}
/*
* Check if the Health Data is to be sent to the server. If the user has enabled tracking, Health Data will be sent once every 24 hours.
* Send Health Data to the server if the period is more than 24 hours.
* We are sending the data as soon as the user launches brackets. The data will be sent to the server only after the notification dialog
* for opt-out/in is closed.
@param forceSend Flag for sending analytics data for testing purpose
*/
function checkHealthDataSend(forceSend) {
var result = new $.Deferred(),
isHDTracking = prefs.get("healthDataTracking"),
nextTimeToSend,
currentTime;
HealthLogger.setHealthLogsEnabled(isHDTracking);
window.clearTimeout(timeoutVar);
if (isHDTracking) {
nextTimeToSend = PreferencesManager.getViewState("nextHealthDataSendTime");
currentTime = Date.now();
// Never send data before FIRST_LAUNCH_SEND_DELAY has ellapsed on a fresh install. This gives the user time to read the notification
// popup, learn more, and opt out if desired
if (!nextTimeToSend) {
nextTimeToSend = currentTime + FIRST_LAUNCH_SEND_DELAY;
PreferencesManager.setViewState("nextHealthDataSendTime", nextTimeToSend);
// don't return yet though - still want to set the timeout below
}
if (currentTime >= nextTimeToSend || forceSend) {
// Bump up nextHealthDataSendTime at the begining of chaining to avoid any chance of sending data again before 24 hours, // e.g. if the server request fails or the code below crashes
PreferencesManager.setViewState("nextHealthDataSendTime", currentTime + ONE_DAY);
sendHealthDataToServer().always(function() {
sendAnalyticsDataToServer()
.done(function () {
// We have already sent the health data, so can clear all health data
// Logged till now
HealthLogger.clearHealthData();
result.resolve();
})
.fail(function () {
result.reject();
})
.always(function () {
timeoutVar = setTimeout(checkHealthDataSend, ONE_DAY);
});
});
} else {
timeoutVar = setTimeout(checkHealthDataSend, nextTimeToSend - currentTime);
result.reject();
}
} else {
result.reject();
}
return result.promise();
}
/**
* Check if the Analytic Data is to be sent to the server.
* If the user has enabled tracking, Analytic Data will be sent once per session
* Send Analytic Data to the server if the Data associated with the given Event is not yet sent in this session.
* We are sending the data as soon as the user triggers the event.
* The data will be sent to the server only after the notification dialog
* for opt-out/in is closed.
* @param{Object} event event object
* @param{Object} Eventparams Object Containg Data to be sent to Server
* @param{boolean} forceSend Flag for sending analytics data for testing purpose
**/
function checkAnalyticsDataSend(event, Eventparams, forceSend) {
var result = new $.Deferred(),
isHDTracking = prefs.get("healthDataTracking"),
isEventDataAlreadySent;
if (isHDTracking) {
isEventDataAlreadySent = HealthLogger.analyticsEventMap.get(Eventparams.eventName);
HealthLogger.analyticsEventMap.set(Eventparams.eventName, true);
if (!isEventDataAlreadySent || forceSend) {
sendAnalyticsDataToServer(Eventparams)
.done(function () {
HealthLogger.analyticsEventMap.set(Eventparams.eventName, true);
result.resolve();
}).fail(function () {
HealthLogger.analyticsEventMap.set(Eventparams.eventName, false);
result.reject();
});
} else {
result.reject();
}
} else {
result.reject();
}
return result.promise();
}
/**
* This function is auto called after 24 hours to empty the map
* Map is used to make sure that we send an event only once per 24 hours
**/
function emptyAnalyticsMap() {
HealthLogger.analyticsEventMap.clear();
setTimeout(emptyAnalyticsMap, ONE_DAY);
}
setTimeout(emptyAnalyticsMap, ONE_DAY);
// Expose a command to test data sending capability, but limit it to dev environment only
CommandManager.register("Sends health data and Analytics data for testing purpose", "sendHealthData", function() {
if (brackets.config.environment === "stage") {
return checkHealthDataSend(true);
} else {
return $.Deferred().reject().promise();
}
});
prefs.on("change", "healthDataTracking", function () {
checkHealthDataSend();
});
HealthLogger.on("SendAnalyticsData", checkAnalyticsDataSend);
window.addEventListener("online", function () {
checkHealthDataSend();
});
window.addEventListener("offline", function () {
window.clearTimeout(timeoutVar);
});
AppInit.appReady(function () {
checkHealthDataSend();
});
exports.getHealthData = getHealthData;
exports.getAnalyticsData = getAnalyticsData;
exports.checkHealthDataSend = checkHealthDataSend;
});