@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.
Files changed (143) hide show
  1. package/android/proguard-rules.pro +15 -0
  2. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1 -1
  3. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +7 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  5. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  6. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +97 -0
  7. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
  8. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +102 -20
  9. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  10. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  11. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  12. package/lib/commonjs/checkForUpdate.js +26 -14
  13. package/lib/commonjs/checkForUpdate.js.map +1 -1
  14. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +89 -25
  15. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  16. package/lib/commonjs/httpClient.js +2 -2
  17. package/lib/commonjs/httpClient.js.map +1 -1
  18. package/lib/commonjs/index.js +32 -11
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.spec.js +59 -14
  21. package/lib/commonjs/index.spec.js.map +1 -1
  22. package/lib/commonjs/native.js +15 -11
  23. package/lib/commonjs/native.js.map +1 -1
  24. package/lib/commonjs/native.spec.js +107 -6
  25. package/lib/commonjs/native.spec.js.map +1 -1
  26. package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +21 -21
  27. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  28. package/lib/commonjs/notifyAppReadyInsights.js +185 -0
  29. package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
  30. package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +102 -29
  31. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
  32. package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  33. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
  34. package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +7 -7
  35. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
  36. package/lib/commonjs/sdkVersion.js +1 -1
  37. package/lib/commonjs/sdkVersion.js.map +1 -1
  38. package/lib/commonjs/wrap.js +10 -9
  39. package/lib/commonjs/wrap.js.map +1 -1
  40. package/lib/commonjs/wrap.spec.js +90 -25
  41. package/lib/commonjs/wrap.spec.js.map +1 -1
  42. package/lib/module/checkForUpdate.js +27 -15
  43. package/lib/module/checkForUpdate.js.map +1 -1
  44. package/lib/module/checkForUpdate.releaseCatalog.spec.js +89 -25
  45. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  46. package/lib/module/httpClient.js +2 -2
  47. package/lib/module/httpClient.js.map +1 -1
  48. package/lib/module/index.js +33 -12
  49. package/lib/module/index.js.map +1 -1
  50. package/lib/module/index.spec.js +59 -14
  51. package/lib/module/index.spec.js.map +1 -1
  52. package/lib/module/native.js +12 -8
  53. package/lib/module/native.js.map +1 -1
  54. package/lib/module/native.spec.js +107 -6
  55. package/lib/module/native.spec.js.map +1 -1
  56. package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +16 -16
  57. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  58. package/lib/module/notifyAppReadyInsights.js +178 -0
  59. package/lib/module/notifyAppReadyInsights.js.map +1 -0
  60. package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +94 -21
  61. package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
  62. package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  63. package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
  64. package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +4 -4
  65. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
  66. package/lib/module/sdkVersion.js +1 -1
  67. package/lib/module/sdkVersion.js.map +1 -1
  68. package/lib/module/wrap.js +10 -9
  69. package/lib/module/wrap.js.map +1 -1
  70. package/lib/module/wrap.spec.js +92 -25
  71. package/lib/module/wrap.spec.js.map +1 -1
  72. package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -1
  73. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/httpClient.d.ts +6 -6
  75. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/index.d.ts +6 -7
  77. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/native.d.ts +9 -6
  79. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
  81. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
  83. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  85. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/wrap.types.d.ts +5 -6
  88. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  89. package/lib/typescript/module/checkForUpdate.d.ts +1 -1
  90. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  91. package/lib/typescript/module/httpClient.d.ts +6 -6
  92. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  93. package/lib/typescript/module/index.d.ts +6 -7
  94. package/lib/typescript/module/index.d.ts.map +1 -1
  95. package/lib/typescript/module/native.d.ts +9 -6
  96. package/lib/typescript/module/native.d.ts.map +1 -1
  97. package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
  98. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
  99. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
  100. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  101. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  102. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  103. package/lib/typescript/module/wrap.d.ts.map +1 -1
  104. package/lib/typescript/module/wrap.types.d.ts +5 -6
  105. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  106. package/package.json +3 -3
  107. package/src/checkForUpdate.releaseCatalog.spec.ts +128 -58
  108. package/src/checkForUpdate.ts +32 -20
  109. package/src/httpClient.ts +11 -11
  110. package/src/index.spec.ts +62 -14
  111. package/src/index.ts +44 -16
  112. package/src/native.spec.ts +110 -6
  113. package/src/native.ts +15 -12
  114. package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +17 -17
  115. package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +81 -22
  116. package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
  117. package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +5 -5
  118. package/src/notifyAppReadyInsights.ts +281 -0
  119. package/src/sdkVersion.ts +1 -1
  120. package/src/wrap.spec.ts +101 -31
  121. package/src/wrap.tsx +11 -9
  122. package/src/wrap.types.ts +5 -6
  123. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  124. package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
  125. package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
  126. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
  127. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  128. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  129. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  130. package/lib/module/notifyAppReadyAnalytics.js +0 -128
  131. package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
  132. package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
  133. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  134. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  135. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
  136. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
  137. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  138. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  139. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
  140. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
  141. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  142. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  143. package/src/notifyAppReadyAnalytics.ts +0 -194
package/src/index.spec.ts CHANGED
@@ -22,7 +22,7 @@ const mocks = vi.hoisted(() => {
22
22
  })),
23
23
  getAppVersion: vi.fn(() => "1.0.0"),
24
24
  getBaseURL: vi.fn(() => null),
25
- getBundleId: vi.fn(() => "bundle-id"),
25
+ getUpdateId: vi.fn(() => "release-id"),
26
26
  getChannel: vi.fn(() => "production"),
27
27
  getCohort: vi.fn(() => "123"),
28
28
  getCrashHistory: vi.fn(() => []),
@@ -30,8 +30,7 @@ const mocks = vi.hoisted(() => {
30
30
  getFingerprintHash: vi.fn(() => null),
31
31
  getInstallId: vi.fn(() => "install-id"),
32
32
  getManifest: vi.fn(() => null),
33
- getMinimumReleaseId: vi.fn(() => "min-bundle-id"),
34
- getReleaseId: vi.fn(async () => null),
33
+ getMinBundleId: vi.fn(() => "min-bundle-id"),
35
34
  init: vi.fn(),
36
35
  isChannelSwitched: vi.fn(() => false),
37
36
  notifyAppReady: vi.fn(() => ({ status: "UNCHANGED" as const })),
@@ -45,6 +44,8 @@ const mocks = vi.hoisted(() => {
45
44
  };
46
45
  });
47
46
 
47
+ vi.mock("react-native", () => ({ Platform: { OS: "ios" } }));
48
+
48
49
  vi.mock("./httpClient", () => ({
49
50
  createHttpClient: mocks.createHttpClient,
50
51
  }));
@@ -57,9 +58,11 @@ vi.mock("./native", () => ({
57
58
  addListener: mocks.addListener,
58
59
  clearCrashHistory: mocks.clearCrashHistory,
59
60
  getPublicActiveUpdateState: mocks.getActiveUpdateState,
61
+ getActiveUpdateState: mocks.getActiveUpdateState,
62
+ getBundleId: () => "bundle-id",
60
63
  getAppVersion: mocks.getAppVersion,
61
64
  getBaseURL: mocks.getBaseURL,
62
- getBundleId: mocks.getBundleId,
65
+ getUpdateId: mocks.getUpdateId,
63
66
  getChannel: mocks.getChannel,
64
67
  getCohort: mocks.getCohort,
65
68
  getCrashHistory: mocks.getCrashHistory,
@@ -67,8 +70,7 @@ vi.mock("./native", () => ({
67
70
  getFingerprintHash: mocks.getFingerprintHash,
68
71
  getInstallId: mocks.getInstallId,
69
72
  getManifest: mocks.getManifest,
70
- getMinimumReleaseId: mocks.getMinimumReleaseId,
71
- getReleaseId: mocks.getReleaseId,
73
+ getMinBundleId: mocks.getMinBundleId,
72
74
  isChannelSwitched: mocks.isChannelSwitched,
73
75
  notifyAppReady: mocks.notifyAppReady,
74
76
  reload: mocks.reload,
@@ -97,13 +99,21 @@ describe("HotUpdater client initialization", () => {
97
99
  mocks.wrap.mockImplementation((Component: unknown) => Component);
98
100
  });
99
101
 
102
+ it("exposes the console ID through the existing getter only", async () => {
103
+ const HotUpdater = await importHotUpdater();
104
+
105
+ expect(HotUpdater.getBundleId()).toBe("release-id");
106
+ expect(HotUpdater).not.toHaveProperty("getReleaseId");
107
+ expect(HotUpdater).not.toHaveProperty("getUpdateId");
108
+ });
109
+
100
110
  it("initializes a private HTTP client from the required baseURL", async () => {
101
111
  const client = { createSession: vi.fn() };
102
112
  mocks.createHttpClient.mockReturnValue(client as never);
103
113
  const HotUpdater = await importHotUpdater();
104
114
 
105
115
  HotUpdater.init({
106
- analytics: true,
116
+ insights: true,
107
117
  baseURL: "https://updates.example.com",
108
118
  requestHeaders: { Authorization: "Bearer token" },
109
119
  requestTimeout: 1000,
@@ -113,7 +123,7 @@ describe("HotUpdater client initialization", () => {
113
123
  "https://updates.example.com",
114
124
  );
115
125
  expect(mocks.init).toHaveBeenCalledWith({
116
- analytics: true,
126
+ insights: true,
117
127
  client,
118
128
  requestHeaders: { Authorization: "Bearer token" },
119
129
  requestTimeout: 1000,
@@ -130,6 +140,44 @@ describe("HotUpdater client initialization", () => {
130
140
  expect(resolveBaseURL).not.toHaveBeenCalled();
131
141
  });
132
142
 
143
+ it("enables insights by default for init and wrap", async () => {
144
+ const HotUpdater = await importHotUpdater();
145
+
146
+ HotUpdater.init({ baseURL: "https://updates.example.com" });
147
+ HotUpdater.wrap({
148
+ baseURL: "https://updates.example.com",
149
+ updateStrategy: "appVersion",
150
+ });
151
+
152
+ expect(mocks.init).toHaveBeenCalledWith(
153
+ expect.objectContaining({ insights: true }),
154
+ );
155
+ expect(mocks.wrap).toHaveBeenCalledWith(
156
+ expect.objectContaining({ insights: true }),
157
+ );
158
+ });
159
+
160
+ it("preserves an explicit insights opt-out", async () => {
161
+ const HotUpdater = await importHotUpdater();
162
+
163
+ HotUpdater.init({
164
+ baseURL: "https://updates.example.com",
165
+ insights: false,
166
+ });
167
+ HotUpdater.wrap({
168
+ baseURL: "https://updates.example.com",
169
+ insights: false,
170
+ updateStrategy: "appVersion",
171
+ });
172
+
173
+ expect(mocks.init).toHaveBeenCalledWith(
174
+ expect.objectContaining({ insights: false }),
175
+ );
176
+ expect(mocks.wrap).toHaveBeenCalledWith(
177
+ expect.objectContaining({ insights: false }),
178
+ );
179
+ });
180
+
133
181
  it("requires baseURL and rejects the removed resolver shape", async () => {
134
182
  const HotUpdater = await importHotUpdater();
135
183
 
@@ -171,13 +219,13 @@ describe("HotUpdater client initialization", () => {
171
219
  expect(wrapOptions.updateStrategy).toBe("appVersion");
172
220
  });
173
221
 
174
- it("merges settings and carries the analytics gate to checks", async () => {
222
+ it("merges settings and carries the insights gate to checks", async () => {
175
223
  const client = { createSession: vi.fn() };
176
224
  mocks.createHttpClient.mockReturnValue(client as never);
177
225
  const checkOnError = vi.fn();
178
226
  const HotUpdater = await importHotUpdater();
179
227
  HotUpdater.init({
180
- analytics: true,
228
+ insights: true,
181
229
  baseURL: "https://updates.example.com",
182
230
  requestHeaders: { Authorization: "Bearer token" },
183
231
  requestTimeout: 1000,
@@ -190,7 +238,7 @@ describe("HotUpdater client initialization", () => {
190
238
  });
191
239
 
192
240
  expect(mocks.checkForUpdate).toHaveBeenCalledWith({
193
- analytics: true,
241
+ insights: true,
194
242
  client,
195
243
  onError: checkOnError,
196
244
  requestHeaders: {
@@ -208,13 +256,13 @@ describe("HotUpdater client initialization", () => {
208
256
  const HotUpdater = await importHotUpdater();
209
257
 
210
258
  HotUpdater.wrap({
211
- analytics: true,
259
+ insights: true,
212
260
  baseURL: "https://updates.example.com",
213
261
  updateStrategy: "appVersion",
214
262
  });
215
263
 
216
264
  expect(mocks.wrap).toHaveBeenCalledWith({
217
- analytics: true,
265
+ insights: true,
218
266
  client,
219
267
  updateStrategy: "appVersion",
220
268
  });
@@ -283,7 +331,7 @@ describe("HotUpdater client initialization", () => {
283
331
  const HotUpdater = await importHotUpdater();
284
332
 
285
333
  expect(HotUpdater.getInstallId()).toBe("install-id");
286
- expect(HotUpdater.getMinimumReleaseId()).toBe("min-bundle-id");
334
+ expect(HotUpdater.getMinBundleId()).toBe("min-bundle-id");
287
335
  HotUpdater.setUser({ userId: "user-123", username: "alice" });
288
336
  expect(mocks.setUser).toHaveBeenCalledWith({
289
337
  userId: "user-123",
package/src/index.ts CHANGED
@@ -7,10 +7,12 @@ import { createHttpClient, type HotUpdaterHttpClient } from "./httpClient";
7
7
  import {
8
8
  addListener,
9
9
  getPublicActiveUpdateState,
10
+ getActiveUpdateState,
11
+ getBundleId,
10
12
  clearCrashHistory,
11
13
  getAppVersion,
12
14
  getBaseURL,
13
- getBundleId,
15
+ getUpdateId,
14
16
  getChannel,
15
17
  getCohort,
16
18
  getCrashHistory,
@@ -18,8 +20,7 @@ import {
18
20
  getFingerprintHash,
19
21
  getInstallId,
20
22
  getManifest,
21
- getMinimumReleaseId,
22
- getReleaseId,
23
+ getMinBundleId,
23
24
  isChannelSwitched,
24
25
  notifyAppReady,
25
26
  reload,
@@ -30,6 +31,7 @@ import {
30
31
  type UpdateParams,
31
32
  updateBundle,
32
33
  } from "./native";
34
+ import { reportBundleDownloaded } from "./notifyAppReadyInsights";
33
35
  import { hotUpdaterStore } from "./store";
34
36
  import {
35
37
  type AutoUpdateOptions,
@@ -101,12 +103,13 @@ function createHotUpdaterClient() {
101
103
 
102
104
  // Global configuration stored from wrap
103
105
  const globalConfig: {
104
- analytics?: boolean;
106
+ insights: boolean;
105
107
  client: HotUpdaterHttpClient | null;
106
108
  requestHeaders?: Record<string, string>;
107
109
  requestTimeout?: number;
108
110
  onError?: (error: unknown) => void;
109
111
  } = {
112
+ insights: true,
110
113
  client: null,
111
114
  };
112
115
 
@@ -158,6 +161,7 @@ function createHotUpdaterClient() {
158
161
  const { baseURL, ...rest } = autoOptions;
159
162
  return {
160
163
  ...rest,
164
+ insights: rest.insights ?? true,
161
165
  client: createHttpClient(baseURL),
162
166
  };
163
167
  }
@@ -177,6 +181,7 @@ function createHotUpdaterClient() {
177
181
  const { baseURL, ...baseURLRest } = rest;
178
182
  return {
179
183
  ...baseURLRest,
184
+ insights: baseURLRest.insights ?? true,
180
185
  client: createHttpClient(baseURL),
181
186
  };
182
187
  }
@@ -188,7 +193,7 @@ function createHotUpdaterClient() {
188
193
  normalizedOptions: InternalInitOptions | InternalWrapOptions,
189
194
  options: HotUpdaterOptions | HotUpdaterInitOptions,
190
195
  ) => {
191
- globalConfig.analytics = normalizedOptions.analytics;
196
+ globalConfig.insights = normalizedOptions.insights ?? true;
192
197
  globalConfig.client = normalizedOptions.client;
193
198
  globalConfig.requestHeaders = options.requestHeaders;
194
199
  globalConfig.requestTimeout = options.requestTimeout;
@@ -333,15 +338,13 @@ function createHotUpdaterClient() {
333
338
  getActiveUpdateState: async () => getPublicActiveUpdateState(),
334
339
 
335
340
  /**
336
- * Fetches the current bundle ID of the app.
341
+ * Returns the selected update ID, matching the ID shown in the console.
342
+ * A staged update can change this ID before the app reloads.
337
343
  */
338
- getBundleId,
344
+ getBundleId: getUpdateId,
339
345
 
340
- /** Preferred Release-catalog name for the build-time UUIDv7 floor. */
341
- getMinimumReleaseId,
342
-
343
- /** Returns the active Release identity, or null for BUILTIN/legacy state. */
344
- getReleaseId,
346
+ /** Returns the minimum bundle ID based on the native app build time. */
347
+ getMinBundleId,
345
348
 
346
349
  /**
347
350
  * Fetches the current manifest for the active bundle.
@@ -448,7 +451,7 @@ function createHotUpdaterClient() {
448
451
 
449
452
  const mergedConfig: InternalCheckForUpdateOptions = {
450
453
  ...config,
451
- analytics: globalConfig.analytics,
454
+ insights: globalConfig.insights,
452
455
  client,
453
456
  requestHeaders: {
454
457
  ...globalConfig.requestHeaders,
@@ -491,9 +494,34 @@ function createHotUpdaterClient() {
491
494
  * }
492
495
  * ```
493
496
  */
494
- updateBundle: (params: UpdateParams) => {
495
- ensureGlobalClient("updateBundle");
496
- return updateBundle(params);
497
+ updateBundle: async (params: UpdateParams) => {
498
+ const client = ensureGlobalClient("updateBundle");
499
+ const fromBundleId = getBundleId();
500
+ const state = getActiveUpdateState();
501
+ const active = state.activeSelection;
502
+ const downloaded = await updateBundle(params);
503
+ if (downloaded && params.fileUrl !== null) {
504
+ const selection = getActiveUpdateState().activeSelection;
505
+ await reportBundleDownloaded(
506
+ { ...globalConfig, client },
507
+ {
508
+ fromBundleId,
509
+ fromReleaseId:
510
+ active?.bundleId === fromBundleId
511
+ ? active.releaseId
512
+ : state.stableSelection?.bundleId === fromBundleId
513
+ ? state.stableSelection.releaseId
514
+ : null,
515
+ toBundleId: params.bundleId,
516
+ toReleaseId: selection?.releaseId ?? null,
517
+ channel: params.channel ?? getChannel(),
518
+ updateStrategy: selection?.scopeKey?.startsWith("v1:fingerprint:")
519
+ ? "fingerprint"
520
+ : "appVersion",
521
+ },
522
+ );
523
+ }
524
+ return downloaded;
497
525
  },
498
526
 
499
527
  /**
@@ -52,6 +52,13 @@ vi.mock("./specs/NativeHotUpdater", () => ({
52
52
  describe("notifyAppReady", () => {
53
53
  beforeEach(() => {
54
54
  vi.resetModules();
55
+ nativeModuleMock.getActiveUpdateState.mockReset();
56
+ nativeModuleMock.getActiveUpdateState.mockReturnValue({
57
+ activeSelection: null,
58
+ stableSelection: null,
59
+ verificationPending: false,
60
+ highestSeenCatalogs: {},
61
+ });
55
62
  nativeModuleMock.notifyAppReady.mockReset();
56
63
  nativeModuleMock.getBaseURL.mockReset();
57
64
  nativeModuleMock.getBundleId.mockReset();
@@ -122,6 +129,89 @@ describe("notifyAppReady", () => {
122
129
  });
123
130
  });
124
131
 
132
+ it("tracks same-file promotions without changing artifact or crash identities", async () => {
133
+ const state = {
134
+ activeSelection: {
135
+ kind: "BUNDLE",
136
+ bundleId: "bundle-123",
137
+ releaseId: "release-1",
138
+ },
139
+ };
140
+ nativeModuleMock.getActiveUpdateState.mockImplementation(() => state);
141
+ nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
142
+ nativeModuleMock.getManifest.mockReturnValue({ assets: {} });
143
+ nativeModuleMock.getCrashHistory.mockReturnValue(["bundle-failed"]);
144
+ const { getUpdateId, getBundleId, getManifest, getCrashHistory } =
145
+ await import("./native");
146
+
147
+ expect(getUpdateId()).toBe("release-1");
148
+ state.activeSelection.releaseId = "release-2";
149
+ expect(getUpdateId()).toBe("release-2");
150
+ expect(getBundleId()).toBe("bundle-123");
151
+ expect(getManifest().bundleId).toBe("bundle-123");
152
+ expect(getCrashHistory()).toEqual(["bundle-failed"]);
153
+ });
154
+
155
+ it.each([
156
+ [null, "min-bundle-id"],
157
+ ["legacy-bundle", "legacy-bundle"],
158
+ ])(
159
+ "preserves the fallback ID for native bundle %s",
160
+ async (bundleId, expected) => {
161
+ nativeModuleMock.getBundleId.mockReturnValue(bundleId);
162
+ const { getUpdateId } = await import("./native");
163
+
164
+ expect(getUpdateId()).toBe(expected);
165
+ },
166
+ );
167
+
168
+ it("reports a staged update while the manifest still identifies the running file", async () => {
169
+ const runningSelection = {
170
+ kind: "BUNDLE",
171
+ bundleId: "running-file",
172
+ releaseId: "running-update",
173
+ };
174
+ const state = {
175
+ activeSelection: runningSelection,
176
+ stableSelection: runningSelection,
177
+ };
178
+ nativeModuleMock.getActiveUpdateState.mockImplementation(() => state);
179
+ nativeModuleMock.getBundleId.mockReturnValue("running-file");
180
+ nativeModuleMock.getManifest.mockReturnValue({
181
+ assets: {},
182
+ bundleId: "running-file",
183
+ });
184
+ const { getUpdateId, getBundleId, getManifest } = await import("./native");
185
+
186
+ expect(getUpdateId()).toBe("running-update");
187
+ state.activeSelection = {
188
+ kind: "BUNDLE",
189
+ bundleId: "staged-file",
190
+ releaseId: "staged-update",
191
+ };
192
+
193
+ expect(getUpdateId()).toBe("staged-update");
194
+ expect(getBundleId()).toBe("running-file");
195
+ expect(getManifest().bundleId).toBe("running-file");
196
+ });
197
+
198
+ it("reports an explicit built-in rollback by its update ID", async () => {
199
+ nativeModuleMock.getActiveUpdateState.mockReturnValue(
200
+ JSON.stringify({
201
+ activeSelection: {
202
+ kind: "EMBEDDED",
203
+ releaseId: "rollback-1",
204
+ bundleId: "min-bundle-id",
205
+ },
206
+ }),
207
+ );
208
+ nativeModuleMock.getBundleId.mockReturnValue(null);
209
+ const { getUpdateId, getBundleId } = await import("./native");
210
+
211
+ expect(getUpdateId()).toBe("rollback-1");
212
+ expect(getBundleId()).toBe("min-bundle-id");
213
+ });
214
+
125
215
  it("normalizes legacy PROMOTED launch reports to UPDATE_APPLIED", async () => {
126
216
  nativeModuleMock.notifyAppReady.mockReturnValue(
127
217
  JSON.stringify({
@@ -157,7 +247,7 @@ describe("notifyAppReady", () => {
157
247
  toBundleId: "bundle-122",
158
248
  });
159
249
  expect(readNotifyAppReady()).toEqual({
160
- analyticsEvent: {
250
+ insightsEvent: {
161
251
  fromBundleId: "bundle-123",
162
252
  toBundleId: "bundle-122",
163
253
  type: "RECOVERED",
@@ -184,13 +274,13 @@ describe("notifyAppReady", () => {
184
274
 
185
275
  expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
186
276
  expect(readNotifyAppReady()).toEqual({
187
- analyticsEvent: null,
277
+ insightsEvent: null,
188
278
  pending: false,
189
279
  result: { status: "UNCHANGED" },
190
280
  });
191
281
  });
192
282
 
193
- it("returns UNCHANGED when automatic analytics metadata is incomplete", async () => {
283
+ it("returns UNCHANGED when automatic insights metadata is incomplete", async () => {
194
284
  nativeModuleMock.notifyAppReady.mockReturnValue({
195
285
  status: "UPDATE_APPLIED",
196
286
  });
@@ -199,7 +289,7 @@ describe("notifyAppReady", () => {
199
289
 
200
290
  expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
201
291
  expect(readNotifyAppReady()).toEqual({
202
- analyticsEvent: null,
292
+ insightsEvent: null,
203
293
  pending: false,
204
294
  result: { status: "UNCHANGED" },
205
295
  });
@@ -212,7 +302,7 @@ describe("notifyAppReady", () => {
212
302
 
213
303
  expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
214
304
  expect(readNotifyAppReady()).toEqual({
215
- analyticsEvent: null,
305
+ insightsEvent: null,
216
306
  pending: false,
217
307
  result: { status: "UNCHANGED" },
218
308
  });
@@ -225,7 +315,7 @@ describe("notifyAppReady", () => {
225
315
 
226
316
  expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
227
317
  expect(readNotifyAppReady()).toEqual({
228
- analyticsEvent: null,
318
+ insightsEvent: null,
229
319
  pending: true,
230
320
  result: { status: "UNCHANGED" },
231
321
  });
@@ -728,6 +818,20 @@ describe("notifyAppReady", () => {
728
818
  expect(getInstallId()).toBe("install-123");
729
819
  });
730
820
 
821
+ it("normalizes an omitted native fingerprint constant to null", async () => {
822
+ nativeModuleMock.getConstants.mockReturnValue({
823
+ APP_VERSION: null,
824
+ CHANNEL: "production",
825
+ DEFAULT_CHANNEL: "production",
826
+ FINGERPRINT_HASH: undefined as unknown as null,
827
+ MIN_BUNDLE_ID: "min-bundle-id",
828
+ });
829
+
830
+ const { getFingerprintHash } = await import("./native");
831
+
832
+ expect(getFingerprintHash()).toBeNull();
833
+ });
834
+
731
835
  it("throws when native SDK does not expose getInstallId", async () => {
732
836
  const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
733
837
  getInstallId?: typeof nativeModuleMock.getInstallId;
package/src/native.ts CHANGED
@@ -215,7 +215,7 @@ const getNativeBundleId = (): string | null => {
215
215
  };
216
216
 
217
217
  const resolveBundleId = (bundleId: string | null): string => {
218
- return !bundleId || bundleId === NIL_UUID ? getMinimumReleaseId() : bundleId;
218
+ return !bundleId || bundleId === NIL_UUID ? getMinBundleId() : bundleId;
219
219
  };
220
220
 
221
221
  const getFreshBundleId = (): string => {
@@ -588,17 +588,20 @@ export function setReloadBehavior(
588
588
  }
589
589
 
590
590
  /**
591
- * Fetches the build-time minimum Release id.
591
+ * Fetches the minimum bundle id created from the native app build time.
592
592
  *
593
- * @returns {string} The minimum Release id.
593
+ * Release Catalog selection uses this value as its minimum Release id.
594
+ *
595
+ * @returns {string} The minimum bundle id.
594
596
  */
595
- export const getMinimumReleaseId = (): string => {
597
+ export const getMinBundleId = (): string => {
596
598
  const constants = HotUpdaterNative.getConstants();
597
599
  return constants.MIN_BUNDLE_ID;
598
600
  };
599
601
 
600
- export const getReleaseId = async (): Promise<string | null> =>
601
- getActiveUpdateState().activeSelection?.releaseId ?? null;
602
+ /** Returns the selected update ID shown in the console, including staged updates. */
603
+ export const getUpdateId = (): string =>
604
+ getActiveUpdateState().activeSelection?.releaseId ?? getBundleId();
602
605
 
603
606
  /**
604
607
  * Fetches the current bundle version id.
@@ -685,7 +688,7 @@ export const isChannelSwitched = (): boolean => {
685
688
  */
686
689
  export const getFingerprintHash = (): string | null => {
687
690
  const constants = HotUpdaterNative.getConstants();
688
- return constants.FINGERPRINT_HASH;
691
+ return constants.FINGERPRINT_HASH ?? null;
689
692
  };
690
693
 
691
694
  export const getInstallId = (): string => {
@@ -781,7 +784,7 @@ export type NotifyAppReadyResult =
781
784
  toReleaseId?: string;
782
785
  };
783
786
 
784
- export type NotifyAppReadyAnalyticsEvent = {
787
+ export type NotifyAppReadyInsightsEvent = {
785
788
  type: "UPDATE_APPLIED" | "RECOVERED";
786
789
  fromBundleId: string;
787
790
  toBundleId: string;
@@ -889,9 +892,9 @@ const getNotifyAppReadyTransition = (
889
892
  return null;
890
893
  };
891
894
 
892
- const getNotifyAppReadyAnalyticsEvent = (
895
+ const getNotifyAppReadyInsightsEvent = (
893
896
  result: RawNotifyAppReadyResult,
894
- ): NotifyAppReadyAnalyticsEvent | null => {
897
+ ): NotifyAppReadyInsightsEvent | null => {
895
898
  const transition = getNotifyAppReadyTransition(result);
896
899
 
897
900
  if (!transition || !isPersistedUpdateStrategy(result.updateStrategy)) {
@@ -936,14 +939,14 @@ const normalizeNotifyAppReadyResult = (
936
939
 
937
940
  export const readNotifyAppReady = (): {
938
941
  result: NotifyAppReadyResult;
939
- analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
942
+ insightsEvent: NotifyAppReadyInsightsEvent | null;
940
943
  pending: boolean;
941
944
  } => {
942
945
  const rawResult = readRawNotifyAppReadyResult();
943
946
 
944
947
  return {
945
948
  result: normalizeNotifyAppReadyResult(rawResult),
946
- analyticsEvent: getNotifyAppReadyAnalyticsEvent(rawResult),
949
+ insightsEvent: getNotifyAppReadyInsightsEvent(rawResult),
947
950
  pending: rawResult.status === "PENDING",
948
951
  };
949
952
  };
@@ -1,13 +1,13 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type {
4
- NotifyAppReadyAnalyticsEvent,
4
+ NotifyAppReadyInsightsEvent,
5
5
  NotifyAppReadyResult,
6
6
  } from "./native";
7
7
  import {
8
8
  createNotifyReadResult,
9
9
  stubNotifyFrame,
10
- } from "./notifyAppReadyAnalytics.test-utils";
10
+ } from "./notifyAppReadyInsights.test-utils";
11
11
 
12
12
  const platformState = vi.hoisted((): { OS: "ios" | "android" } => ({
13
13
  OS: "ios",
@@ -48,7 +48,7 @@ const mocks = vi.hoisted(() => ({
48
48
  getPersistedUserIdentity: vi.fn(() => ({})),
49
49
  readNotifyAppReady: vi.fn<
50
50
  () => {
51
- analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
51
+ insightsEvent: NotifyAppReadyInsightsEvent | null;
52
52
  pending: boolean;
53
53
  result: NotifyAppReadyResult;
54
54
  }
@@ -78,20 +78,20 @@ vi.mock("./specs/NativeHotUpdater", () => ({
78
78
  }));
79
79
 
80
80
  const createClient = () => {
81
- const sendAnalyticsEvent = vi.fn().mockResolvedValue(undefined);
81
+ const sendInsightsEvent = vi.fn().mockResolvedValue(undefined);
82
82
  return {
83
83
  client: {
84
84
  createSession: vi.fn(async () => ({
85
85
  fetchReleaseCatalog: vi.fn(),
86
86
  resolveArtifact: vi.fn(),
87
- sendAnalyticsEvent,
87
+ sendInsightsEvent,
88
88
  })),
89
89
  },
90
- sendAnalyticsEvent,
90
+ sendInsightsEvent,
91
91
  };
92
92
  };
93
93
 
94
- describe("automatic notifyAppReady analytics boundaries", () => {
94
+ describe("automatic notifyAppReady insights boundaries", () => {
95
95
  beforeEach(() => {
96
96
  vi.resetModules();
97
97
  vi.unstubAllGlobals();
@@ -124,7 +124,7 @@ describe("automatic notifyAppReady analytics boundaries", () => {
124
124
  const { init } = await import("./wrap");
125
125
 
126
126
  init({
127
- analytics: true,
127
+ insights: true,
128
128
  client: {
129
129
  createSession: vi.fn(async () => {
130
130
  throw new Error("baseURL unavailable");
@@ -138,7 +138,7 @@ describe("automatic notifyAppReady analytics boundaries", () => {
138
138
  expect(onError).not.toHaveBeenCalled();
139
139
  expect(onNotifyAppReady).toHaveBeenCalledWith({ status: "UNCHANGED" });
140
140
  expect(warn).toHaveBeenCalledWith(
141
- "[HotUpdater] Automatic notifyAppReady analytics failed:",
141
+ "[HotUpdater] Automatic notifyAppReady insights failed:",
142
142
  expect.objectContaining({
143
143
  message: "baseURL unavailable",
144
144
  }),
@@ -163,17 +163,17 @@ describe("automatic notifyAppReady analytics boundaries", () => {
163
163
  } else {
164
164
  mocks.getInstallId.mockReturnValue(value);
165
165
  }
166
- const { client, sendAnalyticsEvent } = createClient();
166
+ const { client, sendInsightsEvent } = createClient();
167
167
  const { init } = await import("./wrap");
168
168
 
169
- init({ analytics: true, client, onError, onNotifyAppReady });
169
+ init({ insights: true, client, onError, onNotifyAppReady });
170
170
  await vi.runOnlyPendingTimersAsync();
171
171
 
172
- expect(sendAnalyticsEvent).not.toHaveBeenCalled();
172
+ expect(sendInsightsEvent).not.toHaveBeenCalled();
173
173
  expect(onError).not.toHaveBeenCalled();
174
174
  expect(onNotifyAppReady).toHaveBeenCalledWith({ status: "UNCHANGED" });
175
175
  expect(warn).toHaveBeenCalledWith(
176
- "[HotUpdater] Automatic notifyAppReady analytics failed:",
176
+ "[HotUpdater] Automatic notifyAppReady insights failed:",
177
177
  expect.any(Error),
178
178
  );
179
179
  warn.mockRestore();
@@ -197,17 +197,17 @@ describe("automatic notifyAppReady analytics boundaries", () => {
197
197
  } else {
198
198
  mocks.getBundleId.mockReturnValue(value);
199
199
  }
200
- const { client, sendAnalyticsEvent } = createClient();
200
+ const { client, sendInsightsEvent } = createClient();
201
201
  const { init } = await import("./wrap");
202
202
 
203
- init({ analytics: true, client, onError, onNotifyAppReady });
203
+ init({ insights: true, client, onError, onNotifyAppReady });
204
204
  await vi.runOnlyPendingTimersAsync();
205
205
 
206
- expect(sendAnalyticsEvent).not.toHaveBeenCalled();
206
+ expect(sendInsightsEvent).not.toHaveBeenCalled();
207
207
  expect(onError).not.toHaveBeenCalled();
208
208
  expect(onNotifyAppReady).toHaveBeenCalledWith({ status: "UNCHANGED" });
209
209
  expect(warn).toHaveBeenCalledWith(
210
- "[HotUpdater] Automatic notifyAppReady analytics failed:",
210
+ "[HotUpdater] Automatic notifyAppReady insights failed:",
211
211
  expect.any(Error),
212
212
  );
213
213
  warn.mockRestore();