diff --git a/code-push-plugin-testing-framework/script/serverUtil.js b/code-push-plugin-testing-framework/script/serverUtil.js index 8050362..30ff5ef 100644 --- a/code-push-plugin-testing-framework/script/serverUtil.js +++ b/code-push-plugin-testing-framework/script/serverUtil.js @@ -31,6 +31,11 @@ function setupServer(targetPlatform) { console.log("Application downloading the package."); res.download(exports.updatePackagePath); }); + app.post("/v0.1/public/codepush/report_status/download", function (req, res) { + console.log("Application reported download status."); + console.log("Body: " + JSON.stringify(req.body)); + res.sendStatus(200); + }); app.post("/reportTestMessage", function (req, res) { console.log("Application reported a test message."); console.log("Body: " + JSON.stringify(req.body)); diff --git a/package-mixins.js b/package-mixins.js index 6df5da5..7bd64ca 100644 --- a/package-mixins.js +++ b/package-mixins.js @@ -1,5 +1,6 @@ import { NativeEventEmitter } from "react-native"; import log from "./logging"; +import { DownloadStatus } from "./lib/acquisition-sdk/acquisition-sdk"; // Reporting this event is important, but avoid blocking install()/restartApp() indefinitely // on a stalled network request. @@ -39,22 +40,35 @@ module.exports = (NativeCodePush) => { ); } + const downloadStartTime = Date.now(); + const reportDownloadStatus = async (status) => { + if (!reportStatusDownload) return; + // Only report a duration on success: on failure, this would be the time until + // the download broke rather than a completed download's duration, and could be misleading. + const downloadDurationMs = status === DownloadStatus.Succeeded ? Date.now() - downloadStartTime : undefined; + try { + await withTimeout(reportStatusDownload({ ...this, downloadDurationMs, status }), REPORT_STATUS_DOWNLOAD_TIMEOUT_MS); + } catch (err) { + log(`Report download status failed: ${err}`); + } + }; + // Use the downloaded package info. Native code will save the package info // so that the client knows what the current package version is. try { const updatePackageCopy = Object.assign({}, this); Object.keys(updatePackageCopy).forEach((key) => (typeof updatePackageCopy[key] === 'function') && delete updatePackageCopy[key]); - const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback); - - if (reportStatusDownload) { - try { - await withTimeout(reportStatusDownload(this), REPORT_STATUS_DOWNLOAD_TIMEOUT_MS); - } catch (err) { - log(`Report download status failed: ${err}`); - } + let downloadedPackage; + try { + downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback); + } catch (err) { + await reportDownloadStatus(DownloadStatus.Failed); + throw err; } + await reportDownloadStatus(DownloadStatus.Succeeded); + return { ...downloadedPackage, ...local }; } finally { downloadProgressSubscription && downloadProgressSubscription.remove(); diff --git a/src/acquisition-sdk/__tests__/acquisition-sdk.test.ts b/src/acquisition-sdk/__tests__/acquisition-sdk.test.ts index 70899c1..454c2be 100644 --- a/src/acquisition-sdk/__tests__/acquisition-sdk.test.ts +++ b/src/acquisition-sdk/__tests__/acquisition-sdk.test.ts @@ -222,7 +222,7 @@ describe("Acquisition SDK", () => { it("reportStatusDownload(...) signals completion", (done: Mocha.Done): void => { var acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.HttpRequester(), configuration); - acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + acquisition.reportStatusDownload({ ...templateCurrentPackage, status: acquisitionSdk.DownloadStatus.Succeeded }, ((error: Error, parameter: void): void => { if (error) { throw error; } @@ -261,7 +261,7 @@ describe("Acquisition SDK", () => { (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; })); - acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + acquisition.reportStatusDownload({ ...templateCurrentPackage, status: acquisitionSdk.DownloadStatus.Succeeded }, ((error: Error, parameter: void): void => { assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, true); acquisition = acquisition = new acquisitionSdk.AcquisitionManager(new mockApi.CustomResponseHttpRequester(invalidJsonResponse), configuration); (acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled = false; @@ -287,7 +287,7 @@ describe("Acquisition SDK", () => { assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); })); - acquisition.reportStatusDownload(templateCurrentPackage, ((error: Error, parameter: void): void => { + acquisition.reportStatusDownload({ ...templateCurrentPackage, status: acquisitionSdk.DownloadStatus.Succeeded }, ((error: Error, parameter: void): void => { assert.strictEqual((acquisitionSdk.AcquisitionManager as any)._apiCallsDisabled, false); })); diff --git a/src/acquisition-sdk/acquisition-sdk.ts b/src/acquisition-sdk/acquisition-sdk.ts index d957d65..d570a2c 100644 --- a/src/acquisition-sdk/acquisition-sdk.ts +++ b/src/acquisition-sdk/acquisition-sdk.ts @@ -1,6 +1,6 @@ // Vendored from https://github.com/microsoft/code-push/blob/master/src/script/acquisition-sdk.ts (archived, MIT licensed) -import { UpdateCheckResponse, UpdateCheckRequest, DeploymentStatusReport, DownloadReport } from "./types"; +import { UpdateCheckResponse, UpdateCheckRequest, DeploymentStatusReport, DownloadReport, DownloadStatusValue } from "./types"; import { CodePushHttpError, CodePushDeployStatusError, CodePushPackageError } from "./code-push-error" export namespace Http { @@ -35,6 +35,11 @@ export interface RemotePackage extends Package { downloadUrl: string; } +export interface DownloadedPackage extends Package { + downloadDurationMs?: number; + status: DownloadStatusValue; +} + export interface NativeUpdateNotification { updateAppVersion: boolean; // Always true appVersion: string; @@ -59,6 +64,11 @@ export class AcquisitionStatus { public static DeploymentFailed = "DeploymentFailed"; } +export class DownloadStatus { + public static Succeeded: DownloadStatusValue = "DownloadSucceeded"; + public static Failed: DownloadStatusValue = "DownloadFailed"; +} + export class AcquisitionManager { private readonly BASE_URL_PART = "appcenter.ms"; private _appVersion: string; @@ -235,7 +245,7 @@ export class AcquisitionManager { }); } - public reportStatusDownload(downloadedPackage: Package, callback?: Callback): void { + public reportStatusDownload(downloadedPackage: DownloadedPackage, callback?: Callback): void { if (AcquisitionManager._apiCallsDisabled) { console.log(`[CodePush] Api calls are disabled, skipping API call`); callback(/*error*/ null, /*not used*/ null); @@ -246,7 +256,11 @@ export class AcquisitionManager { var body: DownloadReport = { client_unique_id: this._clientUniqueId, deployment_key: this._deploymentKey, - label: downloadedPackage.label + label: downloadedPackage.label, + package_hash: downloadedPackage.packageHash, + package_size_bytes: downloadedPackage.packageSize, + download_duration_ms: downloadedPackage.downloadDurationMs, + status: downloadedPackage.status }; this._httpRequester.request(Http.Verb.POST, url, JSON.stringify(body), (error: Error, response: Http.Response): void => { diff --git a/src/acquisition-sdk/types.ts b/src/acquisition-sdk/types.ts index 1ba2a79..798f45e 100644 --- a/src/acquisition-sdk/types.ts +++ b/src/acquisition-sdk/types.ts @@ -13,11 +13,17 @@ export interface DeploymentStatusReport { status?: string; } +export type DownloadStatusValue = "DownloadSucceeded" | "DownloadFailed"; + /*in*/ export interface DownloadReport { client_unique_id: string; deployment_key: string; label: string; + package_hash: string; + package_size_bytes: number; + download_duration_ms?: number; + status: DownloadStatusValue; } /*out*/