@hot-updater/react-native 1.0.0-rc.1 → 1.0.0-rc.15

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