From d132593458880044d7863b234b4352952eb2860c Mon Sep 17 00:00:00 2001 From: Alexander Bodalevsky Date: Fri, 16 Jun 2017 13:58:52 +0300 Subject: [PATCH] Windows: Implemented TelemetryManager (#812) * Examples updated to RNW 0.43.0 * Initial implementation * interim commit * getUpdateReport - completed * getBinaryUpdateReport - completed * getRetryStatusReport - completed * getRollbackReport - completed * recordStatusReported - completed saveStatusReportForRetry - completed * Commented unused variables * Fixed telemetry report * Optimization: run telemetry in async mode * neat fixes * react-native-windows updated to 0.43.0-rc.0 * neat fix * added async to some ReactMEthod calls --- .../CodePush.Net46.Test.csproj | 14 +- .../TelemetryManagerTest.cs | 117 ++++++++ windows/CodePush.Net46.Test/app.config | 11 + windows/CodePush.Net46.Test/packages.config | 4 + windows/CodePush.Net46/UpdateManager.cs | 3 - .../CodePush.Shared/CodePush.Shared.projitems | 1 + .../CodePush.Shared/CodePushNativeModule.cs | 71 ++++- .../CodePush.Shared/CodePushReactPackage.cs | 13 +- windows/CodePush.Shared/SettingsManager.cs | 15 ++ windows/CodePush.Shared/TelemetryManager.cs | 250 ++++++++++++++++++ 10 files changed, 486 insertions(+), 13 deletions(-) create mode 100644 windows/CodePush.Net46.Test/TelemetryManagerTest.cs create mode 100644 windows/CodePush.Net46.Test/app.config create mode 100644 windows/CodePush.Net46.Test/packages.config create mode 100644 windows/CodePush.Shared/TelemetryManager.cs diff --git a/windows/CodePush.Net46.Test/CodePush.Net46.Test.csproj b/windows/CodePush.Net46.Test/CodePush.Net46.Test.csproj index 8366f05..5c8b63d 100644 --- a/windows/CodePush.Net46.Test/CodePush.Net46.Test.csproj +++ b/windows/CodePush.Net46.Test/CodePush.Net46.Test.csproj @@ -71,6 +71,11 @@ MinimumRecommendedRules.ruleset + + False + ..\..\Examples\CodePushDemoApp\windows\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + @@ -81,13 +86,16 @@ - + + False + + @@ -95,6 +103,10 @@ CodePush.Net46 + + + + diff --git a/windows/CodePush.Net46.Test/TelemetryManagerTest.cs b/windows/CodePush.Net46.Test/TelemetryManagerTest.cs new file mode 100644 index 0000000..682b45f --- /dev/null +++ b/windows/CodePush.Net46.Test/TelemetryManagerTest.cs @@ -0,0 +1,117 @@ +using CodePush.ReactNative; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CodePush.Net46.Test +{ + /// + /// Some tests for telemetry manager + /// As implementation of TelemetryManager was ported from android version, we do not test logic here. + /// Here are tests for some tricky parts of implementation, or check some data transformation that + /// has no full equvalent in C# + /// + [TestClass] + public class TelemetryManagerTest + { + #region Constants from TelemetryManager + //private static readonly string APP_VERSION_KEY = "appVersion"; + private static readonly string DEPLOYMENT_FAILED_STATUS = "DeploymentFailed"; + private static readonly string DEPLOYMENT_KEY_KEY = "deploymentKey"; + private static readonly string DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded"; + private static readonly string LABEL_KEY = "label"; + private static readonly string LAST_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_LAST_DEPLOYMENT_REPORT"; + //private static readonly string PACKAGE_KEY = "package"; + //private static readonly string PREVIOUS_DEPLOYMENT_KEY_KEY = "previousDeploymentKey"; + //private static readonly string PREVIOUS_LABEL_OR_APP_VERSION_KEY = "previousLabelOrAppVersion"; + private static readonly string RETRY_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_RETRY_DEPLOYMENT_REPORT"; + private static readonly string STATUS_KEY = "status"; + #endregion + + [TestMethod] + public void TestGetUpdateReportNoPreviousUpdate() + { + var input = new JObject(); + input.Add(DEPLOYMENT_KEY_KEY, "depKeyParam"); + input.Add(LABEL_KEY, "labelParam"); + + var output = TelemetryManager.GetUpdateReport(input); + Assert.IsNotNull(output); + Assert.IsTrue(output.ToString(Formatting.None).Contains("\"status\":\"DeploymentSucceeded\"")); + } + + [TestMethod] + public void TestGetUpdateReportWithPreviousUpdate() + { + SettingsManager.SetString(LAST_DEPLOYMENT_REPORT_KEY, "prevKey:prevLabel"); + var input = new JObject(); + input.Add(DEPLOYMENT_KEY_KEY, "depKeyParam"); + input.Add(LABEL_KEY, "labelParam"); + + var output = TelemetryManager.GetUpdateReport(input); + Assert.IsNotNull(output); + Assert.IsTrue(output.ToString(Formatting.None).Contains("\"status\":\"DeploymentSucceeded\"")); + Assert.IsTrue(output.ToString(Formatting.None).Contains("\"previousDeploymentKey\":\"prevKey\",\"previousLabelOrAppVersion\":\"prevLabel\"")); + + //Clean Up + SettingsManager.RemoveString(LAST_DEPLOYMENT_REPORT_KEY); + } + + [TestMethod] + public void TestGetUpdateReportNegative() + { + var inputNoLabel = new JObject(); + inputNoLabel.Add(DEPLOYMENT_KEY_KEY, "depKeyParam"); + Assert.IsNull(TelemetryManager.GetUpdateReport(inputNoLabel)); + + var inputNoKey = new JObject(); + inputNoKey.Add(LABEL_KEY, "labelParam"); + Assert.IsNull(TelemetryManager.GetUpdateReport(inputNoKey)); + } + + [TestMethod] + public void TestRecordStatusReportWithRollback() + { + var report = new JObject(); + report.Add(STATUS_KEY, DEPLOYMENT_FAILED_STATUS); + + TelemetryManager.RecordStatusReported(report); + Assert.IsTrue(true); + } + + [TestMethod] + public void TestRecordStatusReportWithoutRollback() + { + var reportSuccess = new JObject(); + reportSuccess.Add(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS); + TelemetryManager.RecordStatusReported(reportSuccess); + + var reportNoStatus = new JObject(); + TelemetryManager.RecordStatusReported(reportNoStatus); + + Assert.IsTrue(true); + } + + [TestMethod] + public void TestStatusReportForRetrySerialization() + { + SettingsManager.RemoveString(RETRY_DEPLOYMENT_REPORT_KEY); + var original = new JObject(); + original.Add("keyString", "stringValue"); + original.Add("keyInt", 42); + original.Add("keyBool", true); + + TelemetryManager.SaveStatusReportForRetry(original); + + var stringified = SettingsManager.GetString(RETRY_DEPLOYMENT_REPORT_KEY); + SettingsManager.RemoveString(RETRY_DEPLOYMENT_REPORT_KEY); + + Assert.IsNotNull(stringified); + var result = JObject.Parse(stringified); + + Assert.IsTrue((bool)result.GetValue("keyBool")); + Assert.AreEqual(42, (int)result.GetValue("keyInt")); + Assert.AreEqual("stringValue", (string)result.GetValue("keyString")); + } + } +} diff --git a/windows/CodePush.Net46.Test/app.config b/windows/CodePush.Net46.Test/app.config new file mode 100644 index 0000000..cacd4cd --- /dev/null +++ b/windows/CodePush.Net46.Test/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/windows/CodePush.Net46.Test/packages.config b/windows/CodePush.Net46.Test/packages.config new file mode 100644 index 0000000..90aba30 --- /dev/null +++ b/windows/CodePush.Net46.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/windows/CodePush.Net46/UpdateManager.cs b/windows/CodePush.Net46/UpdateManager.cs index a0a3ea4..f184d37 100644 --- a/windows/CodePush.Net46/UpdateManager.cs +++ b/windows/CodePush.Net46/UpdateManager.cs @@ -8,9 +8,6 @@ using System.IO.Compression; using System.Runtime.CompilerServices; using System.Threading.Tasks; - -[assembly: InternalsVisibleTo("CodePush.Net46.UnitTest")] - namespace CodePush.ReactNative { internal class UpdateManager diff --git a/windows/CodePush.Shared/CodePush.Shared.projitems b/windows/CodePush.Shared/CodePush.Shared.projitems index 5f85c52..f5efc24 100644 --- a/windows/CodePush.Shared/CodePush.Shared.projitems +++ b/windows/CodePush.Shared/CodePush.Shared.projitems @@ -16,6 +16,7 @@ + \ No newline at end of file diff --git a/windows/CodePush.Shared/CodePushNativeModule.cs b/windows/CodePush.Shared/CodePushNativeModule.cs index 81c9fd1..e2b0cb7 100644 --- a/windows/CodePush.Shared/CodePushNativeModule.cs +++ b/windows/CodePush.Shared/CodePushNativeModule.cs @@ -21,7 +21,8 @@ namespace CodePush.ReactNative private MinimumBackgroundListener _minimumBackgroundListener; private ReactContext _reactContext; - public CodePushNativeModule(ReactContext reactContext, CodePushReactPackage codePush) : base(reactContext) + public CodePushNativeModule(ReactContext reactContext, CodePushReactPackage codePush) + : base(reactContext) { _reactContext = reactContext; _codePush = codePush; @@ -174,10 +175,60 @@ namespace CodePush.ReactNative [ReactMethod] - public void getNewStatusReport(IPromise promise) + public async void getNewStatusReport(IPromise promise) { - // TODO implement this - promise.Resolve(""); + await Task.Run(() => + { + if (_codePush.NeedToReportRollback) + { + _codePush.NeedToReportRollback = false; + + var failedUpdates = SettingsManager.GetFailedUpdates(); + if (failedUpdates != null && failedUpdates.Count > 0) + { + var lastFailedPackage = (JObject)failedUpdates[failedUpdates.Count - 1]; + var failedStatusReport = TelemetryManager.GetRollbackReport(lastFailedPackage); + if (failedStatusReport != null) + { + promise.Resolve(failedStatusReport); + return; + } + } + } + else if (_codePush.DidUpdate) + { + var currentPackage = _codePush.UpdateManager.GetCurrentPackageAsync().Result; + if (currentPackage != null) + { + var newPackageStatusReport = TelemetryManager.GetUpdateReport(currentPackage); + if (newPackageStatusReport != null) + { + promise.Resolve(newPackageStatusReport); + return; + } + } + } + else if (_codePush.IsRunningBinaryVersion) + { + var newAppVersionStatusReport = TelemetryManager.GetBinaryUpdateReport(_codePush.AppVersion); + if (newAppVersionStatusReport != null) + { + promise.Resolve(newAppVersionStatusReport); + return; + } + } + else + { + var retryStatusReport = TelemetryManager.GetRetryStatusReport(); + if (retryStatusReport != null) + { + promise.Resolve(retryStatusReport); + return; + } + } + + promise.Resolve(""); + }).ConfigureAwait(false); } [ReactMethod] @@ -245,6 +296,18 @@ namespace CodePush.ReactNative } } + [ReactMethod] + public async void recordStatusReported(JObject statusReport) + { + await Task.Run(() => TelemetryManager.RecordStatusReported(statusReport)).ConfigureAwait(false); + } + + [ReactMethod] + public async void saveStatusReportForRetry(JObject statusReport) + { + await Task.Run(() => TelemetryManager.SaveStatusReportForRetry(statusReport)).ConfigureAwait(false); + } + internal async Task LoadBundleAsync() { // #1) Get the private ReactInstanceManager, which is what includes diff --git a/windows/CodePush.Shared/CodePushReactPackage.cs b/windows/CodePush.Shared/CodePushReactPackage.cs index 3374953..a14bf96 100644 --- a/windows/CodePush.Shared/CodePushReactPackage.cs +++ b/windows/CodePush.Shared/CodePushReactPackage.cs @@ -17,8 +17,9 @@ namespace CodePush.ReactNative internal string AppVersion { get; private set; } internal string DeploymentKey { get; private set; } internal string AssetsBundleFileName { get; private set; } - internal bool DidUpdate { get; private set; } - internal bool IsRunningBinaryVersion { get; set; } + internal bool NeedToReportRollback { get; set; } = false; + internal bool DidUpdate { get; private set; } = false; + internal bool IsRunningBinaryVersion { get; private set; } = false; internal ReactPage MainPage { get; private set; } internal UpdateManager UpdateManager { get; private set; } @@ -28,9 +29,6 @@ namespace CodePush.ReactNative DeploymentKey = deploymentKey; MainPage = mainPage; UpdateManager = new UpdateManager(); - IsRunningBinaryVersion = false; - // TODO implement telemetryManager - // _codePushTelemetryManager = new CodePushTelemetryManager(this.applicationContext, CODE_PUSH_PREFERENCES); if (CurrentInstance != null) { @@ -129,6 +127,10 @@ namespace CodePush.ReactNative internal void InitializeUpdateAfterRestart() { + // Reset the state which indicates that + // the app was just freshly updated. + DidUpdate = false; + JObject pendingUpdate = SettingsManager.GetPendingUpdate(); if (pendingUpdate != null) { @@ -138,6 +140,7 @@ namespace CodePush.ReactNative // Pending update was initialized, but notifyApplicationReady was not called. // Therefore, deduce that it is a broken update and rollback. CodePushUtils.Log("Update did not finish loading the last time, rolling back to a previous version."); + NeedToReportRollback = true; RollbackPackageAsync().Wait(); } else diff --git a/windows/CodePush.Shared/SettingsManager.cs b/windows/CodePush.Shared/SettingsManager.cs index c68e914..9e62517 100644 --- a/windows/CodePush.Shared/SettingsManager.cs +++ b/windows/CodePush.Shared/SettingsManager.cs @@ -129,5 +129,20 @@ namespace CodePush.ReactNative Settings.Values[CodePushConstants.PendingUpdateKey] = JsonConvert.SerializeObject(pendingUpdate); } + + internal static void SetString(string key, string value) + { + Settings.Values[key] = value; + } + + internal static string GetString(string key) + { + return (string)Settings.Values[key]; + } + + internal static void RemoveString(string key) + { + Settings.Values.Remove(key); + } } } \ No newline at end of file diff --git a/windows/CodePush.Shared/TelemetryManager.cs b/windows/CodePush.Shared/TelemetryManager.cs new file mode 100644 index 0000000..a67bc96 --- /dev/null +++ b/windows/CodePush.Shared/TelemetryManager.cs @@ -0,0 +1,250 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("CodePush.Net46.Test")] + +namespace CodePush.ReactNative +{ + /// + /// Implementation is ported from + /// android\app\src\main\java\com\microsoft\codepush\react\CodePushTelemetry.java + /// I've tried to leave all logic, comments and structure without significant modification. + /// + + internal class TelemetryManager + { + #region Constants + private static readonly string APP_VERSION_KEY = "appVersion"; + private static readonly string DEPLOYMENT_FAILED_STATUS = "DeploymentFailed"; + private static readonly string DEPLOYMENT_KEY_KEY = "deploymentKey"; + private static readonly string DEPLOYMENT_SUCCEEDED_STATUS = "DeploymentSucceeded"; + private static readonly string LABEL_KEY = "label"; + private static readonly string LAST_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_LAST_DEPLOYMENT_REPORT"; + private static readonly string PACKAGE_KEY = "package"; + private static readonly string PREVIOUS_DEPLOYMENT_KEY_KEY = "previousDeploymentKey"; + private static readonly string PREVIOUS_LABEL_OR_APP_VERSION_KEY = "previousLabelOrAppVersion"; + private static readonly string RETRY_DEPLOYMENT_REPORT_KEY = "CODE_PUSH_RETRY_DEPLOYMENT_REPORT"; + private static readonly string STATUS_KEY = "status"; + #endregion + + #region Internal methods + + internal static JObject GetBinaryUpdateReport(string appVersion) + { + var previousStatusReportIdentifier = GetPreviousStatusReportIdentifier(); + + if (previousStatusReportIdentifier == null) + { + ClearRetryStatusReport(); + + var report = new JObject(); + report.Add(APP_VERSION_KEY, appVersion); + return report; + } + + if (!previousStatusReportIdentifier.Equals(appVersion)) + { + ClearRetryStatusReport(); + + var report = new JObject(); + report.Add(APP_VERSION_KEY, appVersion); + if (IsStatusReportIdentifierCodePushLabel(previousStatusReportIdentifier)) + { + var previousDeploymentKey = GetDeploymentKeyFromStatusReportIdentifier(previousStatusReportIdentifier); + var previousLabel = GetVersionLabelFromStatusReportIdentifier(previousStatusReportIdentifier); + + report.Add(PREVIOUS_DEPLOYMENT_KEY_KEY, previousDeploymentKey); + report.Add(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousLabel); + } + else + { + // Previous status report was with a binary app version. + report.Add(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousStatusReportIdentifier); + } + return report; + } + + return null; + } + + internal static JObject GetRetryStatusReport() + { + var retryStatusReportString = SettingsManager.GetString(RETRY_DEPLOYMENT_REPORT_KEY); + + if (retryStatusReportString != null) + { + ClearRetryStatusReport(); + try + { + var report = JObject.Parse(retryStatusReportString); + return report; + } + catch (Exception) + { + //TODO: should be reported error + } + } + + return null; + } + + internal static JObject GetRollbackReport(JObject lastFailedPackage) + { + var report = new JObject(); + report.Add(STATUS_KEY, DEPLOYMENT_FAILED_STATUS); + report.Add(PACKAGE_KEY, lastFailedPackage); + + return report; + } + + internal static JObject GetUpdateReport(JObject currentPackage) + { + var currentPackageIdentifier = GetPackageStatusReportIdentifier(currentPackage); + if (currentPackageIdentifier == null) + { + return null; + } + + var previousStatusReportIdentifier = GetPreviousStatusReportIdentifier(); + if (previousStatusReportIdentifier == null) + { + ClearRetryStatusReport(); + var report = new JObject(); + report.Add(PACKAGE_KEY, currentPackage); + report.Add(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS); + return report; + } + + if (!previousStatusReportIdentifier.Equals(currentPackageIdentifier)) + { + ClearRetryStatusReport(); + var report = new JObject(); + report.Add(PACKAGE_KEY, currentPackage); + report.Add(STATUS_KEY, DEPLOYMENT_SUCCEEDED_STATUS); + + if (IsStatusReportIdentifierCodePushLabel(previousStatusReportIdentifier)) + { + var previousDeploymentKey = GetDeploymentKeyFromStatusReportIdentifier(previousStatusReportIdentifier); + var previousLabel = GetVersionLabelFromStatusReportIdentifier(previousStatusReportIdentifier); + + report.Add(PREVIOUS_DEPLOYMENT_KEY_KEY, previousDeploymentKey); + report.Add(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousLabel); + } + else + { + // Previous status report was with a binary app version. + report.Add(PREVIOUS_LABEL_OR_APP_VERSION_KEY, previousStatusReportIdentifier); + } + + return report; + } + + return null; + } + + internal static void RecordStatusReported(JObject statusReport) + { + // We don't need to record rollback reports, so exit early if that's what was specified. + var status = (string)statusReport.GetValue(STATUS_KEY); + if ((!string.IsNullOrEmpty(status)) && DEPLOYMENT_FAILED_STATUS.Equals(status)) + { + return; + } + + var appVersion = (string)statusReport.GetValue(APP_VERSION_KEY); + if (!string.IsNullOrEmpty(appVersion)) + { + SaveStatusReportedForIdentifier(appVersion); + } + else + { + var package = (JObject)statusReport.GetValue(PACKAGE_KEY); + if (package == null) + { + return; + } + + var packageIdentifier = GetPackageStatusReportIdentifier(package); + SaveStatusReportedForIdentifier(packageIdentifier); + } + } + + internal static void SaveStatusReportForRetry(JObject statusReport) + { + SettingsManager.SetString(RETRY_DEPLOYMENT_REPORT_KEY, statusReport.ToString(Formatting.None)); + } + + #endregion + + #region Private methods + static string GetPackageStatusReportIdentifier(JObject updatePackage) + { + // Because deploymentKeys can be dynamically switched, we use a + // combination of the deploymentKey and label as the packageIdentifier. + try + { + var deploymentKey = (string)updatePackage[DEPLOYMENT_KEY_KEY]; + var label = (string)updatePackage[LABEL_KEY]; + if (string.IsNullOrEmpty(deploymentKey) || string.IsNullOrEmpty(label)) + { + return null; + } + + return $"{deploymentKey}:{label}"; + } + catch + { + return null; + } + } + + static string GetPreviousStatusReportIdentifier() + { + return SettingsManager.GetString(LAST_DEPLOYMENT_REPORT_KEY); + } + + static private void ClearRetryStatusReport() + { + SettingsManager.RemoveString(RETRY_DEPLOYMENT_REPORT_KEY); + } + + static bool IsStatusReportIdentifierCodePushLabel(string statusReportIdentifier) + { + return (!string.IsNullOrEmpty(statusReportIdentifier)) && statusReportIdentifier.Contains(":"); + } + + static string GetDeploymentKeyFromStatusReportIdentifier(string statusReportIdentifier) + { + string[] parsedIdentifier = statusReportIdentifier.Split(':'); + if (parsedIdentifier.Length > 0) + { + return parsedIdentifier[0]; + } + else + { + return null; + } + } + + static string GetVersionLabelFromStatusReportIdentifier(string statusReportIdentifier) + { + string[] parsedIdentifier = statusReportIdentifier.Split(':'); + if (parsedIdentifier.Length > 1) + { + return parsedIdentifier[1]; + } + else + { + return null; + } + } + + static void SaveStatusReportedForIdentifier(string appVersionOrPackageIdentifier) + { + SettingsManager.SetString(LAST_DEPLOYMENT_REPORT_KEY, appVersionOrPackageIdentifier); + } + #endregion + } +}