@hot-updater/react-native 1.0.0-rc.0 → 1.0.0-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/proguard-rules.pro +15 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1 -1
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +7 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
- package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +97 -0
- package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +102 -20
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
- package/lib/commonjs/checkForUpdate.js +26 -14
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +89 -25
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/httpClient.js +2 -2
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +32 -11
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +59 -14
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +15 -11
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +107 -6
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +21 -21
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
- package/lib/commonjs/notifyAppReadyInsights.js +185 -0
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +102 -29
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +7 -7
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +1 -1
- package/lib/commonjs/wrap.js +10 -9
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +90 -25
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +27 -15
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +89 -25
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/httpClient.js +2 -2
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +33 -12
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +59 -14
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +12 -8
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +107 -6
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +16 -16
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
- package/lib/module/notifyAppReadyInsights.js +178 -0
- package/lib/module/notifyAppReadyInsights.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +94 -21
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
- package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +4 -4
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +1 -1
- package/lib/module/wrap.js +10 -9
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +92 -25
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -6
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +6 -7
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -6
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +5 -6
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -6
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +6 -7
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -6
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +5 -6
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +128 -58
- package/src/checkForUpdate.ts +32 -20
- package/src/httpClient.ts +11 -11
- package/src/index.spec.ts +62 -14
- package/src/index.ts +44 -16
- package/src/native.spec.ts +110 -6
- package/src/native.ts +15 -12
- package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +17 -17
- package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +81 -22
- package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
- package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +5 -5
- package/src/notifyAppReadyInsights.ts +281 -0
- package/src/sdkVersion.ts +1 -1
- package/src/wrap.spec.ts +101 -31
- package/src/wrap.tsx +11 -9
- package/src/wrap.types.ts +5 -6
- package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
- package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.js +0 -128
- package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
- package/src/notifyAppReadyAnalytics.ts +0 -194
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
NotifyAppReadyInsightsEvent,
|
|
5
5
|
NotifyAppReadyResult,
|
|
6
6
|
} from "./native";
|
|
7
7
|
import {
|
|
8
8
|
createNotifyReadResult,
|
|
9
9
|
stubNotifyFrame,
|
|
10
|
-
} from "./
|
|
10
|
+
} from "./notifyAppReadyInsights.test-utils";
|
|
11
11
|
|
|
12
12
|
vi.mock("react-native", () => ({
|
|
13
13
|
Platform: { OS: "ios" },
|
|
@@ -25,7 +25,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
25
25
|
getPersistedUserIdentity: vi.fn(() => ({})),
|
|
26
26
|
readNotifyAppReady: vi.fn<
|
|
27
27
|
() => {
|
|
28
|
-
|
|
28
|
+
insightsEvent: NotifyAppReadyInsightsEvent | null;
|
|
29
29
|
pending: boolean;
|
|
30
30
|
result: NotifyAppReadyResult;
|
|
31
31
|
}
|
|
@@ -51,20 +51,20 @@ vi.mock("./native", () => ({
|
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
53
|
const createClient = () => {
|
|
54
|
-
const
|
|
54
|
+
const sendInsightsEvent = vi.fn().mockResolvedValue(undefined);
|
|
55
55
|
return {
|
|
56
56
|
client: {
|
|
57
57
|
createSession: vi.fn(async () => ({
|
|
58
58
|
fetchReleaseCatalog: vi.fn(),
|
|
59
59
|
resolveArtifact: vi.fn(),
|
|
60
|
-
|
|
60
|
+
sendInsightsEvent,
|
|
61
61
|
})),
|
|
62
62
|
},
|
|
63
|
-
|
|
63
|
+
sendInsightsEvent,
|
|
64
64
|
};
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
describe("automatic notifyAppReady
|
|
67
|
+
describe("automatic notifyAppReady insights", () => {
|
|
68
68
|
beforeEach(() => {
|
|
69
69
|
vi.resetModules();
|
|
70
70
|
vi.unstubAllGlobals();
|
|
@@ -85,6 +85,65 @@ describe("automatic notifyAppReady analytics", () => {
|
|
|
85
85
|
mocks.readNotifyAppReady.mockReturnValue(createNotifyReadResult());
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it("orders download after in-flight startup reporting and suppresses a later no-change report", async () => {
|
|
89
|
+
const { handleNotifyAppReady, reportBundleDownloaded, reportNoChange } =
|
|
90
|
+
await import("./notifyAppReadyInsights");
|
|
91
|
+
const { client, sendInsightsEvent } = createClient();
|
|
92
|
+
let finishStartup!: () => void;
|
|
93
|
+
sendInsightsEvent.mockImplementationOnce(
|
|
94
|
+
() =>
|
|
95
|
+
new Promise<void>((resolve) => {
|
|
96
|
+
finishStartup = resolve;
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
const options = { client, insights: true };
|
|
100
|
+
const startup = handleNotifyAppReady(options);
|
|
101
|
+
await vi.waitFor(() => expect(sendInsightsEvent).toHaveBeenCalledOnce());
|
|
102
|
+
const download = reportBundleDownloaded(options, {
|
|
103
|
+
fromBundleId: "bundle-id",
|
|
104
|
+
fromReleaseId: null,
|
|
105
|
+
toBundleId: "next-bundle",
|
|
106
|
+
toReleaseId: "next-release",
|
|
107
|
+
channel: "production",
|
|
108
|
+
updateStrategy: "appVersion",
|
|
109
|
+
});
|
|
110
|
+
await Promise.resolve();
|
|
111
|
+
expect(sendInsightsEvent).toHaveBeenCalledOnce();
|
|
112
|
+
finishStartup();
|
|
113
|
+
await Promise.all([startup, download]);
|
|
114
|
+
await reportNoChange(options);
|
|
115
|
+
expect(sendInsightsEvent.mock.calls.map(([params]) => params.type)).toEqual(
|
|
116
|
+
["UNCHANGED", "UPDATE_DOWNLOADED"],
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("reports selecting a previous download again after a different pending bundle", async () => {
|
|
121
|
+
const { reportBundleDownloaded } = await import("./notifyAppReadyInsights");
|
|
122
|
+
const { client, sendInsightsEvent } = createClient();
|
|
123
|
+
const transition = {
|
|
124
|
+
fromBundleId: "bundle-id",
|
|
125
|
+
fromReleaseId: null,
|
|
126
|
+
toBundleId: "next-bundle",
|
|
127
|
+
toReleaseId: "next-release",
|
|
128
|
+
channel: "production",
|
|
129
|
+
updateStrategy: "appVersion" as const,
|
|
130
|
+
};
|
|
131
|
+
await reportBundleDownloaded({ client, insights: true }, transition);
|
|
132
|
+
await reportBundleDownloaded({ client, insights: true }, transition);
|
|
133
|
+
await reportBundleDownloaded(
|
|
134
|
+
{ client, insights: true },
|
|
135
|
+
{
|
|
136
|
+
...transition,
|
|
137
|
+
toBundleId: "other-bundle",
|
|
138
|
+
toReleaseId: "other-release",
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
await reportBundleDownloaded({ client, insights: true }, transition);
|
|
142
|
+
expect(
|
|
143
|
+
sendInsightsEvent.mock.calls.map(([params]) => params.toBundleId),
|
|
144
|
+
).toEqual(["next-bundle", "other-bundle", "next-bundle"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
88
147
|
it("sends UPDATE_APPLIED with transition metadata and invokes readiness", async () => {
|
|
89
148
|
stubNotifyFrame();
|
|
90
149
|
mocks.getPersistedUserIdentity.mockReturnValue({
|
|
@@ -107,12 +166,12 @@ describe("automatic notifyAppReady analytics", () => {
|
|
|
107
166
|
),
|
|
108
167
|
);
|
|
109
168
|
|
|
110
|
-
const { client,
|
|
169
|
+
const { client, sendInsightsEvent } = createClient();
|
|
111
170
|
const onNotifyAppReady = vi.fn();
|
|
112
171
|
const { init } = await import("./wrap");
|
|
113
172
|
|
|
114
173
|
init({
|
|
115
|
-
|
|
174
|
+
insights: true,
|
|
116
175
|
onNotifyAppReady,
|
|
117
176
|
requestHeaders: { Authorization: "Bearer token" },
|
|
118
177
|
requestTimeout: 1000,
|
|
@@ -120,7 +179,7 @@ describe("automatic notifyAppReady analytics", () => {
|
|
|
120
179
|
});
|
|
121
180
|
await vi.runOnlyPendingTimersAsync();
|
|
122
181
|
|
|
123
|
-
expect(
|
|
182
|
+
expect(sendInsightsEvent).toHaveBeenCalledWith({
|
|
124
183
|
appVersion: "1.0.0",
|
|
125
184
|
channel: "production",
|
|
126
185
|
cohort: "123",
|
|
@@ -162,14 +221,14 @@ describe("automatic notifyAppReady analytics", () => {
|
|
|
162
221
|
},
|
|
163
222
|
),
|
|
164
223
|
);
|
|
165
|
-
const { client,
|
|
224
|
+
const { client, sendInsightsEvent } = createClient();
|
|
166
225
|
const onNotifyAppReady = vi.fn();
|
|
167
226
|
const { init } = await import("./wrap");
|
|
168
227
|
|
|
169
|
-
init({
|
|
228
|
+
init({ insights: true, client, onNotifyAppReady });
|
|
170
229
|
await vi.runOnlyPendingTimersAsync();
|
|
171
230
|
|
|
172
|
-
expect(
|
|
231
|
+
expect(sendInsightsEvent).toHaveBeenCalledWith({
|
|
173
232
|
appVersion: "1.0.0",
|
|
174
233
|
channel: "production",
|
|
175
234
|
cohort: "123",
|
|
@@ -194,16 +253,16 @@ describe("automatic notifyAppReady analytics", () => {
|
|
|
194
253
|
|
|
195
254
|
it("sends one UNCHANGED event for repeated init calls", async () => {
|
|
196
255
|
stubNotifyFrame();
|
|
197
|
-
const { client,
|
|
256
|
+
const { client, sendInsightsEvent } = createClient();
|
|
198
257
|
const onNotifyAppReady = vi.fn();
|
|
199
258
|
const { init } = await import("./wrap");
|
|
200
259
|
|
|
201
|
-
init({
|
|
202
|
-
init({
|
|
260
|
+
init({ insights: true, client, onNotifyAppReady });
|
|
261
|
+
init({ insights: true, client, onNotifyAppReady });
|
|
203
262
|
await vi.runOnlyPendingTimersAsync();
|
|
204
263
|
|
|
205
|
-
expect(
|
|
206
|
-
expect(
|
|
264
|
+
expect(sendInsightsEvent).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(sendInsightsEvent).toHaveBeenCalledWith({
|
|
207
266
|
appVersion: "1.0.0",
|
|
208
267
|
channel: "production",
|
|
209
268
|
cohort: "123",
|
|
@@ -228,16 +287,16 @@ describe("automatic notifyAppReady analytics", () => {
|
|
|
228
287
|
});
|
|
229
288
|
});
|
|
230
289
|
|
|
231
|
-
it("skips automatic
|
|
290
|
+
it("skips automatic insights when disabled while preserving readiness", async () => {
|
|
232
291
|
stubNotifyFrame();
|
|
233
|
-
const { client,
|
|
292
|
+
const { client, sendInsightsEvent } = createClient();
|
|
234
293
|
const onNotifyAppReady = vi.fn();
|
|
235
294
|
const { init } = await import("./wrap");
|
|
236
295
|
|
|
237
|
-
init({
|
|
296
|
+
init({ insights: false, client, onNotifyAppReady });
|
|
238
297
|
await vi.runOnlyPendingTimersAsync();
|
|
239
298
|
|
|
240
|
-
expect(
|
|
299
|
+
expect(sendInsightsEvent).not.toHaveBeenCalled();
|
|
241
300
|
expect(onNotifyAppReady).toHaveBeenCalledWith({ status: "UNCHANGED" });
|
|
242
301
|
});
|
|
243
302
|
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
NotifyAppReadyInsightsEvent,
|
|
5
5
|
NotifyAppReadyResult,
|
|
6
6
|
} from "./native";
|
|
7
7
|
|
|
8
8
|
export const createNotifyReadResult = (
|
|
9
9
|
result: NotifyAppReadyResult = { status: "UNCHANGED" },
|
|
10
|
-
|
|
10
|
+
insightsEvent: NotifyAppReadyInsightsEvent | null = null,
|
|
11
11
|
pending = false,
|
|
12
12
|
): {
|
|
13
|
-
|
|
13
|
+
insightsEvent: NotifyAppReadyInsightsEvent | null;
|
|
14
14
|
pending: boolean;
|
|
15
15
|
result: NotifyAppReadyResult;
|
|
16
16
|
} => ({
|
|
17
|
-
|
|
17
|
+
insightsEvent,
|
|
18
18
|
pending,
|
|
19
19
|
result,
|
|
20
20
|
});
|
package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts}
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
NotifyAppReadyInsightsEvent,
|
|
5
5
|
NotifyAppReadyResult,
|
|
6
6
|
} from "./native";
|
|
7
7
|
import {
|
|
8
8
|
createNotifyReadResult,
|
|
9
9
|
stubNotifyFrame,
|
|
10
|
-
} from "./
|
|
10
|
+
} from "./notifyAppReadyInsights.test-utils";
|
|
11
11
|
|
|
12
12
|
vi.mock("react-native", () => ({
|
|
13
13
|
Platform: { OS: "ios" },
|
|
@@ -36,7 +36,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
36
36
|
getPersistedUserIdentity: vi.fn(() => ({})),
|
|
37
37
|
readNotifyAppReady: vi.fn<
|
|
38
38
|
() => {
|
|
39
|
-
|
|
39
|
+
insightsEvent: NotifyAppReadyInsightsEvent | null;
|
|
40
40
|
pending: boolean;
|
|
41
41
|
result: NotifyAppReadyResult;
|
|
42
42
|
}
|
|
@@ -110,7 +110,7 @@ describe("automatic notifyAppReady transport failures", () => {
|
|
|
110
110
|
"https://updates.example.test/hot-updater",
|
|
111
111
|
);
|
|
112
112
|
|
|
113
|
-
init({
|
|
113
|
+
init({ insights: true, client, onError, onNotifyAppReady });
|
|
114
114
|
await vi.runOnlyPendingTimersAsync();
|
|
115
115
|
|
|
116
116
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
@@ -119,7 +119,7 @@ describe("automatic notifyAppReady transport failures", () => {
|
|
|
119
119
|
status: "UNCHANGED",
|
|
120
120
|
});
|
|
121
121
|
expect(warn).toHaveBeenCalledWith(
|
|
122
|
-
"[HotUpdater] Automatic notifyAppReady
|
|
122
|
+
"[HotUpdater] Automatic notifyAppReady insights failed:",
|
|
123
123
|
expect.any(Error),
|
|
124
124
|
);
|
|
125
125
|
} finally {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
import type { HotUpdaterError } from "./error";
|
|
4
|
+
import type { InsightsEventParams, HotUpdaterHttpClient } from "./httpClient";
|
|
5
|
+
import {
|
|
6
|
+
getAppVersion,
|
|
7
|
+
getBundleId,
|
|
8
|
+
getChannel,
|
|
9
|
+
getCohort,
|
|
10
|
+
getFingerprintHash,
|
|
11
|
+
getInstallId,
|
|
12
|
+
getPersistedUserIdentity,
|
|
13
|
+
type NotifyAppReadyInsightsEvent,
|
|
14
|
+
type NotifyAppReadyResult,
|
|
15
|
+
readNotifyAppReady,
|
|
16
|
+
} from "./native";
|
|
17
|
+
|
|
18
|
+
export type NotifyAppReadyOptions = {
|
|
19
|
+
insights?: boolean;
|
|
20
|
+
client: HotUpdaterHttpClient;
|
|
21
|
+
requestHeaders?: Record<string, string>;
|
|
22
|
+
requestTimeout?: number;
|
|
23
|
+
reportUnchanged?: boolean;
|
|
24
|
+
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
25
|
+
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type RequestAnimationFrame = (callback: (timestamp: number) => void) => number;
|
|
29
|
+
|
|
30
|
+
let didAttemptAutomaticInsights = false;
|
|
31
|
+
let didDownload = false;
|
|
32
|
+
let readinessPromise: Promise<NotifyAppReadyResult | undefined> | undefined;
|
|
33
|
+
|
|
34
|
+
const waitForNextFrame = () =>
|
|
35
|
+
new Promise<void>((resolve) => {
|
|
36
|
+
const requestAnimationFrame = (
|
|
37
|
+
globalThis as typeof globalThis & {
|
|
38
|
+
requestAnimationFrame?: RequestAnimationFrame;
|
|
39
|
+
}
|
|
40
|
+
)?.requestAnimationFrame;
|
|
41
|
+
|
|
42
|
+
if (requestAnimationFrame) {
|
|
43
|
+
requestAnimationFrame(() => resolve());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void Promise.resolve().then(resolve);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const assertNever = (value: never): never => {
|
|
51
|
+
throw new Error(`[HotUpdater] Unexpected notifyAppReady status: ${value}`);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const buildNotifyAppReadyInsightsParams = (
|
|
55
|
+
nativeResult: NotifyAppReadyResult,
|
|
56
|
+
insightsEvent: NotifyAppReadyInsightsEvent | null,
|
|
57
|
+
options: Pick<NotifyAppReadyOptions, "requestHeaders" | "requestTimeout">,
|
|
58
|
+
): InsightsEventParams => {
|
|
59
|
+
const appVersion = getAppVersion();
|
|
60
|
+
|
|
61
|
+
if (!appVersion) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"[HotUpdater] Automatic insights requires a non-null native app version.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { userId, username } = getPersistedUserIdentity();
|
|
68
|
+
const installId = getInstallId();
|
|
69
|
+
|
|
70
|
+
if (!installId) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"[HotUpdater] Automatic insights requires a non-null native install id.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const platform: "ios" | "android" =
|
|
77
|
+
Platform.OS === "android" ? "android" : "ios";
|
|
78
|
+
|
|
79
|
+
const commonParams = {
|
|
80
|
+
appVersion,
|
|
81
|
+
channel: getChannel(),
|
|
82
|
+
cohort: getCohort(),
|
|
83
|
+
fingerprintHash: getFingerprintHash(),
|
|
84
|
+
fromReleaseId: insightsEvent?.fromReleaseId ?? null,
|
|
85
|
+
installId,
|
|
86
|
+
platform,
|
|
87
|
+
requestHeaders: options.requestHeaders,
|
|
88
|
+
requestTimeout: options.requestTimeout,
|
|
89
|
+
toReleaseId: insightsEvent?.toReleaseId ?? null,
|
|
90
|
+
...(userId != null ? { userId } : {}),
|
|
91
|
+
...(username != null ? { username } : {}),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
switch (nativeResult.status) {
|
|
95
|
+
case "UNCHANGED": {
|
|
96
|
+
const bundleId = getBundleId();
|
|
97
|
+
|
|
98
|
+
if (!bundleId) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"[HotUpdater] Automatic insights requires a non-null current bundle id.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...commonParams,
|
|
106
|
+
fromBundleId: null,
|
|
107
|
+
toBundleId: bundleId,
|
|
108
|
+
type: "UNCHANGED",
|
|
109
|
+
updateStrategy: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
case "UPDATE_APPLIED":
|
|
113
|
+
case "RECOVERED":
|
|
114
|
+
if (!insightsEvent) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"[HotUpdater] Native launch report is missing persisted metadata required for automatic insights.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
switch (insightsEvent.type) {
|
|
121
|
+
case "UPDATE_APPLIED":
|
|
122
|
+
return {
|
|
123
|
+
...commonParams,
|
|
124
|
+
fromBundleId: insightsEvent.fromBundleId,
|
|
125
|
+
toBundleId: insightsEvent.toBundleId,
|
|
126
|
+
type: "UPDATE_APPLIED",
|
|
127
|
+
updateStrategy: insightsEvent.updateStrategy,
|
|
128
|
+
};
|
|
129
|
+
case "RECOVERED":
|
|
130
|
+
return {
|
|
131
|
+
...commonParams,
|
|
132
|
+
fromBundleId: insightsEvent.fromBundleId,
|
|
133
|
+
toBundleId: insightsEvent.toBundleId,
|
|
134
|
+
type: "RECOVERED",
|
|
135
|
+
updateStrategy: insightsEvent.updateStrategy,
|
|
136
|
+
};
|
|
137
|
+
default:
|
|
138
|
+
return assertNever(insightsEvent.type);
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
return assertNever(nativeResult);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let lastInsightsReport = Promise.resolve();
|
|
146
|
+
const sendInsights = (
|
|
147
|
+
options: Pick<NotifyAppReadyOptions, "client">,
|
|
148
|
+
params: InsightsEventParams,
|
|
149
|
+
): Promise<void> => {
|
|
150
|
+
lastInsightsReport = lastInsightsReport
|
|
151
|
+
.catch(() => {})
|
|
152
|
+
.then(async () => {
|
|
153
|
+
const session = await options.client.createSession();
|
|
154
|
+
await session.sendInsightsEvent(params);
|
|
155
|
+
});
|
|
156
|
+
return lastInsightsReport;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const maybeSendAutomaticInsights = async (
|
|
160
|
+
options: NotifyAppReadyOptions,
|
|
161
|
+
nativeResult: NotifyAppReadyResult,
|
|
162
|
+
insightsEvent: NotifyAppReadyInsightsEvent | null,
|
|
163
|
+
): Promise<void> => {
|
|
164
|
+
if (
|
|
165
|
+
!options.insights ||
|
|
166
|
+
didAttemptAutomaticInsights ||
|
|
167
|
+
(nativeResult.status === "UNCHANGED" &&
|
|
168
|
+
(options.reportUnchanged === false || didDownload))
|
|
169
|
+
) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
didAttemptAutomaticInsights = true;
|
|
174
|
+
|
|
175
|
+
if (nativeResult.status !== "UNCHANGED" && !insightsEvent) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"[HotUpdater] Native launch report is missing persisted metadata required for automatic insights.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await sendInsights(
|
|
182
|
+
options,
|
|
183
|
+
buildNotifyAppReadyInsightsParams(nativeResult, insightsEvent, options),
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const notifyAppReady = async (
|
|
188
|
+
options: NotifyAppReadyOptions,
|
|
189
|
+
): Promise<NotifyAppReadyResult | undefined> => {
|
|
190
|
+
try {
|
|
191
|
+
let nativeReadResult: ReturnType<typeof readNotifyAppReady>;
|
|
192
|
+
do {
|
|
193
|
+
await waitForNextFrame();
|
|
194
|
+
nativeReadResult = readNotifyAppReady();
|
|
195
|
+
} while (nativeReadResult.pending);
|
|
196
|
+
|
|
197
|
+
const { insightsEvent, result: nativeResult } = nativeReadResult;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await maybeSendAutomaticInsights(options, nativeResult, insightsEvent);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const warning = error instanceof Error ? error : new Error(String(error));
|
|
203
|
+
console.warn(
|
|
204
|
+
"[HotUpdater] Automatic notifyAppReady insights failed:",
|
|
205
|
+
warning,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
options.onNotifyAppReady?.(nativeResult);
|
|
210
|
+
return nativeResult;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const normalizedError =
|
|
213
|
+
error instanceof Error ? error : new Error(String(error));
|
|
214
|
+
options.onError?.(error);
|
|
215
|
+
console.warn("[HotUpdater] Failed to notify app ready:", normalizedError);
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const handleNotifyAppReady = (
|
|
221
|
+
options: NotifyAppReadyOptions,
|
|
222
|
+
): Promise<NotifyAppReadyResult | undefined> => {
|
|
223
|
+
readinessPromise = notifyAppReady(options);
|
|
224
|
+
return readinessPromise;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Called after an automatic update check finds no download or transition. */
|
|
228
|
+
export const reportNoChange = async (
|
|
229
|
+
options: NotifyAppReadyOptions,
|
|
230
|
+
): Promise<void> => {
|
|
231
|
+
try {
|
|
232
|
+
await maybeSendAutomaticInsights(options, { status: "UNCHANGED" }, null);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.warn("[HotUpdater] Automatic no-change insights failed:", error);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
let lastDownloadedSelection: string | undefined;
|
|
239
|
+
|
|
240
|
+
/** Report only after the archive/diff has been verified and staged successfully. */
|
|
241
|
+
export const reportBundleDownloaded = async (
|
|
242
|
+
options: Pick<
|
|
243
|
+
NotifyAppReadyOptions,
|
|
244
|
+
"insights" | "client" | "requestHeaders" | "requestTimeout"
|
|
245
|
+
>,
|
|
246
|
+
transition: {
|
|
247
|
+
readonly fromBundleId: string;
|
|
248
|
+
readonly fromReleaseId: string | null;
|
|
249
|
+
readonly toBundleId: string;
|
|
250
|
+
readonly toReleaseId: string | null;
|
|
251
|
+
readonly channel: string;
|
|
252
|
+
readonly updateStrategy: "fingerprint" | "appVersion";
|
|
253
|
+
},
|
|
254
|
+
): Promise<void> => {
|
|
255
|
+
if (!options.insights || transition.fromBundleId === transition.toBundleId)
|
|
256
|
+
return;
|
|
257
|
+
const key = JSON.stringify([
|
|
258
|
+
transition.channel,
|
|
259
|
+
transition.toReleaseId,
|
|
260
|
+
transition.toBundleId,
|
|
261
|
+
]);
|
|
262
|
+
if (lastDownloadedSelection === key) return;
|
|
263
|
+
lastDownloadedSelection = key;
|
|
264
|
+
// A later init/mount in this JS runtime must not overwrite pending apply with UNCHANGED.
|
|
265
|
+
didDownload = true;
|
|
266
|
+
try {
|
|
267
|
+
await readinessPromise;
|
|
268
|
+
const common = buildNotifyAppReadyInsightsParams(
|
|
269
|
+
{ status: "UNCHANGED" },
|
|
270
|
+
null,
|
|
271
|
+
options,
|
|
272
|
+
);
|
|
273
|
+
await sendInsights(options, {
|
|
274
|
+
...common,
|
|
275
|
+
...transition,
|
|
276
|
+
type: "UPDATE_DOWNLOADED",
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.warn("[HotUpdater] Download insights failed:", error);
|
|
280
|
+
}
|
|
281
|
+
};
|
package/src/sdkVersion.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.14";
|