@hot-updater/plugin-insights 1.0.0-rc.28 → 1.0.0-rc.29

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.
@@ -31,4 +31,5 @@ const readErrorDetails = (error) => {
31
31
  };
32
32
  };
33
33
  //#endregion
34
+ exports.boundedText = boundedText;
34
35
  exports.readErrorDetails = readErrorDetails;
@@ -31,4 +31,4 @@ const readErrorDetails = (error) => {
31
31
  };
32
32
  };
33
33
  //#endregion
34
- export { readErrorDetails };
34
+ export { boundedText, readErrorDetails };
@@ -46,8 +46,13 @@ const insights = (options = {}) => {
46
46
  const now = context.now();
47
47
  const installId = context.installId;
48
48
  const userId = state.readUserId();
49
+ const response = state.readHttpResponse(movement.channel, appVersion);
49
50
  const body = {
50
51
  ...movement,
52
+ ...response ? { metadata: {
53
+ ...movement.metadata,
54
+ httpResponse: response
55
+ } } : {},
51
56
  eventId: eventId?.(now, installId) ?? require_eventId.createUUIDv7(),
52
57
  installId,
53
58
  ...userId === null ? {} : { userId },
@@ -176,6 +181,17 @@ const insights = (options = {}) => {
176
181
  failureKey
177
182
  });
178
183
  };
184
+ const onHttpResponse = (response) => {
185
+ if (context === null || state === null || context.appVersion === null) return;
186
+ const body = response.body === null ? null : require_errorDetails.boundedText(response.body, 4096);
187
+ state.recordHttpResponse(context.getChannel(), context.appVersion, {
188
+ ...response,
189
+ path: require_errorDetails.boundedText(response.path, 1024),
190
+ body,
191
+ bodyTruncated: response.bodyTruncated || body !== response.body,
192
+ receivedAtMs: context.now()
193
+ });
194
+ };
179
195
  const admit = (body) => {
180
196
  const event = pending.get(body);
181
197
  if (event === void 0 || state === null || context === null) return false;
@@ -220,7 +236,8 @@ const insights = (options = {}) => {
220
236
  onAppReady,
221
237
  onUpdateCheck,
222
238
  onBundleDownloaded,
223
- onUpdateError
239
+ onUpdateError,
240
+ onHttpResponse
224
241
  };
225
242
  },
226
243
  setUser(user) {
@@ -1,4 +1,4 @@
1
- import { readErrorDetails } from "./errorDetails.mjs";
1
+ import { boundedText, readErrorDetails } from "./errorDetails.mjs";
2
2
  import { createKeyedUUIDv7, createUUIDv7 } from "./eventId.mjs";
3
3
  import { createInsightsEventSender } from "./sender.mjs";
4
4
  import { createInsightsState, utcDay, utcDayStart } from "./state.mjs";
@@ -45,8 +45,13 @@ const insights = (options = {}) => {
45
45
  const now = context.now();
46
46
  const installId = context.installId;
47
47
  const userId = state.readUserId();
48
+ const response = state.readHttpResponse(movement.channel, appVersion);
48
49
  const body = {
49
50
  ...movement,
51
+ ...response ? { metadata: {
52
+ ...movement.metadata,
53
+ httpResponse: response
54
+ } } : {},
50
55
  eventId: eventId?.(now, installId) ?? createUUIDv7(),
51
56
  installId,
52
57
  ...userId === null ? {} : { userId },
@@ -175,6 +180,17 @@ const insights = (options = {}) => {
175
180
  failureKey
176
181
  });
177
182
  };
183
+ const onHttpResponse = (response) => {
184
+ if (context === null || state === null || context.appVersion === null) return;
185
+ const body = response.body === null ? null : boundedText(response.body, 4096);
186
+ state.recordHttpResponse(context.getChannel(), context.appVersion, {
187
+ ...response,
188
+ path: boundedText(response.path, 1024),
189
+ body,
190
+ bodyTruncated: response.bodyTruncated || body !== response.body,
191
+ receivedAtMs: context.now()
192
+ });
193
+ };
178
194
  const admit = (body) => {
179
195
  const event = pending.get(body);
180
196
  if (event === void 0 || state === null || context === null) return false;
@@ -219,7 +235,8 @@ const insights = (options = {}) => {
219
235
  onAppReady,
220
236
  onUpdateCheck,
221
237
  onBundleDownloaded,
222
- onUpdateError
238
+ onUpdateError,
239
+ onHttpResponse
223
240
  };
224
241
  },
225
242
  setUser(user) {
@@ -102,6 +102,24 @@ const createInsightsState = (storage) => {
102
102
  keys: [...keys, key].slice(-32)
103
103
  }));
104
104
  },
105
+ readHttpResponse(channel, appVersion) {
106
+ try {
107
+ const saved = JSON.parse(read("httpResponse") ?? "null");
108
+ if (!isRecord(saved) || saved.channel !== channel || saved.appVersion !== appVersion || !isRecord(saved.response)) return void 0;
109
+ const response = saved.response;
110
+ if (response.resource !== "catalog" && response.resource !== "artifact" || typeof response.path !== "string" || response.path.length > 1024 || typeof response.status !== "number" || !Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !isNullableString(response.body) || JSON.stringify(response.body).length > 4096 || typeof response.bodyTruncated !== "boolean" || typeof response.receivedAtMs !== "number" || !Number.isSafeInteger(response.receivedAtMs) || response.receivedAtMs < 0) return void 0;
111
+ return response;
112
+ } catch {
113
+ return;
114
+ }
115
+ },
116
+ recordHttpResponse(channel, appVersion, response) {
117
+ write("httpResponse", JSON.stringify({
118
+ channel,
119
+ appVersion,
120
+ response
121
+ }));
122
+ },
105
123
  readUserId() {
106
124
  try {
107
125
  const user = JSON.parse(read("user") ?? "null");
@@ -102,6 +102,24 @@ const createInsightsState = (storage) => {
102
102
  keys: [...keys, key].slice(-32)
103
103
  }));
104
104
  },
105
+ readHttpResponse(channel, appVersion) {
106
+ try {
107
+ const saved = JSON.parse(read("httpResponse") ?? "null");
108
+ if (!isRecord(saved) || saved.channel !== channel || saved.appVersion !== appVersion || !isRecord(saved.response)) return void 0;
109
+ const response = saved.response;
110
+ if (response.resource !== "catalog" && response.resource !== "artifact" || typeof response.path !== "string" || response.path.length > 1024 || typeof response.status !== "number" || !Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !isNullableString(response.body) || JSON.stringify(response.body).length > 4096 || typeof response.bodyTruncated !== "boolean" || typeof response.receivedAtMs !== "number" || !Number.isSafeInteger(response.receivedAtMs) || response.receivedAtMs < 0) return void 0;
111
+ return response;
112
+ } catch {
113
+ return;
114
+ }
115
+ },
116
+ recordHttpResponse(channel, appVersion, response) {
117
+ write("httpResponse", JSON.stringify({
118
+ channel,
119
+ appVersion,
120
+ response
121
+ }));
122
+ },
105
123
  readUserId() {
106
124
  try {
107
125
  const user = JSON.parse(read("user") ?? "null");
@@ -1,5 +1,6 @@
1
1
  import { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, DatabaseBundleEventMetadata } from "./eventRow.cjs";
2
2
  import { InsightsScope } from "./modelTypes.cjs";
3
+ import { UpdateHttpResponse } from "@hot-updater/protocol";
3
4
  //#region src/server/domain.d.ts
4
5
  /**
5
6
  * Where an update failed and why, as a report carries it. Unknown values of
@@ -33,6 +34,9 @@ type CreateBundleEventRequestBase = {
33
34
  * server creates the ID, and a retry counts as another report.
34
35
  */
35
36
  readonly eventId?: string;
37
+ readonly metadata?: {
38
+ readonly httpResponse?: InsightsHttpResponse;
39
+ };
36
40
  readonly installId: string;
37
41
  readonly toBundleId: string;
38
42
  readonly userId?: string;
@@ -94,6 +98,7 @@ type EventHistoryRow = {
94
98
  readonly channel: string;
95
99
  readonly cohort: string;
96
100
  readonly receivedAtMs: number;
101
+ readonly httpResponse?: InsightsHttpResponse;
97
102
  /** `UPDATE_FAILED`: where the update failed and why. */
98
103
  readonly failure?: BundleEventFailureInput;
99
104
  /** `UPDATE_DOWNLOADED`: how the bundle arrived, when the client said. */
@@ -107,7 +112,11 @@ type InstallationHistoryRow = EventHistoryRow & {
107
112
  readonly type: "UPDATE_DOWNLOADED" | "UPDATE_APPLIED" | "RECOVERED" | "UPDATE_FAILED";
108
113
  readonly fromBundleId: string;
109
114
  };
115
+ type InsightsHttpResponse = UpdateHttpResponse & {
116
+ readonly receivedAtMs: number;
117
+ };
110
118
  type InstallationRow = {
119
+ readonly httpResponse?: InsightsHttpResponse;
111
120
  readonly installId: string;
112
121
  readonly userId: string | null;
113
122
  readonly lastKnownBundleId: string;
@@ -151,4 +160,4 @@ type ReportingOverview = InsightsScope & {
151
160
  };
152
161
  };
153
162
  //#endregion
154
- export { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InstallationHistoryRow, InstallationRow, ReportingOverview };
163
+ export { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InsightsHttpResponse, InstallationHistoryRow, InstallationRow, ReportingOverview };
@@ -1,5 +1,6 @@
1
1
  import { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, DatabaseBundleEventMetadata } from "./eventRow.mjs";
2
2
  import { InsightsScope } from "./modelTypes.mjs";
3
+ import { UpdateHttpResponse } from "@hot-updater/protocol";
3
4
  //#region src/server/domain.d.ts
4
5
  /**
5
6
  * Where an update failed and why, as a report carries it. Unknown values of
@@ -33,6 +34,9 @@ type CreateBundleEventRequestBase = {
33
34
  * server creates the ID, and a retry counts as another report.
34
35
  */
35
36
  readonly eventId?: string;
37
+ readonly metadata?: {
38
+ readonly httpResponse?: InsightsHttpResponse;
39
+ };
36
40
  readonly installId: string;
37
41
  readonly toBundleId: string;
38
42
  readonly userId?: string;
@@ -94,6 +98,7 @@ type EventHistoryRow = {
94
98
  readonly channel: string;
95
99
  readonly cohort: string;
96
100
  readonly receivedAtMs: number;
101
+ readonly httpResponse?: InsightsHttpResponse;
97
102
  /** `UPDATE_FAILED`: where the update failed and why. */
98
103
  readonly failure?: BundleEventFailureInput;
99
104
  /** `UPDATE_DOWNLOADED`: how the bundle arrived, when the client said. */
@@ -107,7 +112,11 @@ type InstallationHistoryRow = EventHistoryRow & {
107
112
  readonly type: "UPDATE_DOWNLOADED" | "UPDATE_APPLIED" | "RECOVERED" | "UPDATE_FAILED";
108
113
  readonly fromBundleId: string;
109
114
  };
115
+ type InsightsHttpResponse = UpdateHttpResponse & {
116
+ readonly receivedAtMs: number;
117
+ };
110
118
  type InstallationRow = {
119
+ readonly httpResponse?: InsightsHttpResponse;
111
120
  readonly installId: string;
112
121
  readonly userId: string | null;
113
122
  readonly lastKnownBundleId: string;
@@ -151,4 +160,4 @@ type ReportingOverview = InsightsScope & {
151
160
  };
152
161
  };
153
162
  //#endregion
154
- export { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InstallationHistoryRow, InstallationRow, ReportingOverview };
163
+ export { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InsightsHttpResponse, InstallationHistoryRow, InstallationRow, ReportingOverview };
@@ -98,6 +98,17 @@ function readFailure(value) {
98
98
  ...previousProcessExit === void 0 ? {} : { previousProcessExit }
99
99
  };
100
100
  }
101
+ function readHttpResponse(value) {
102
+ if (!isRecord(value) || value.resource !== "catalog" && value.resource !== "artifact" || typeof value.path !== "string" || value.path.length > 1024 || !Number.isSafeInteger(value.status) || value.status < 100 || value.status > 599 || value.body !== null && (typeof value.body !== "string" || new TextEncoder().encode(JSON.stringify(value.body)).byteLength > 4096) || typeof value.bodyTruncated !== "boolean" || !Number.isSafeInteger(value.receivedAtMs) || value.receivedAtMs < 0) throw new require_errors.InsightsBadRequestError("Invalid HTTP response details");
103
+ return {
104
+ resource: value.resource,
105
+ path: value.path,
106
+ status: value.status,
107
+ body: value.body,
108
+ bodyTruncated: value.bodyTruncated,
109
+ receivedAtMs: value.receivedAtMs
110
+ };
111
+ }
101
112
  async function readBoundedText(request) {
102
113
  const contentLength = request.headers.get("content-length");
103
114
  const declaredByteLength = Number(contentLength);
@@ -133,7 +144,10 @@ function requireEvent(payload) {
133
144
  const platform = requireStringField(payload, "platform");
134
145
  if (platform !== "ios" && platform !== "android") throw new require_errors.InsightsBadRequestError("Invalid event field: platform");
135
146
  const eventId = readEventId(payload);
147
+ const metadata = readMetadata(payload);
148
+ const httpMetadata = metadata.httpResponse === void 0 ? {} : { httpResponse: readHttpResponse(metadata.httpResponse) };
136
149
  const base = {
150
+ ...metadata.httpResponse === void 0 ? {} : { metadata: httpMetadata },
137
151
  ...eventId === void 0 ? {} : { eventId },
138
152
  installId: requireIdentityField(payload, "installId"),
139
153
  toBundleId: requireStringField(payload, "toBundleId"),
@@ -157,11 +171,11 @@ function requireEvent(payload) {
157
171
  updateStrategy
158
172
  };
159
173
  };
160
- const metadata = readMetadata(payload);
161
174
  switch (type) {
162
175
  case "UPDATE_DOWNLOADED": {
163
176
  const delivery = metadata.delivery;
164
177
  const read = {
178
+ ...httpMetadata,
165
179
  ...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
166
180
  ...metadata.patchFallback === true ? { patchFallback: true } : {}
167
181
  };
@@ -180,13 +194,19 @@ function requireEvent(payload) {
180
194
  return {
181
195
  ...movement(),
182
196
  type,
183
- ...previousProcessExit === void 0 ? {} : { metadata: { previousProcessExit } }
197
+ ...previousProcessExit === void 0 ? {} : { metadata: {
198
+ ...httpMetadata,
199
+ previousProcessExit
200
+ } }
184
201
  };
185
202
  }
186
203
  case "UPDATE_FAILED": return {
187
204
  ...movement(),
188
205
  type,
189
- metadata: { failure: readFailure(metadata.failure) }
206
+ metadata: {
207
+ ...httpMetadata,
208
+ failure: readFailure(metadata.failure)
209
+ }
190
210
  };
191
211
  case "UNCHANGED":
192
212
  if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new require_errors.InsightsBadRequestError("Invalid unchanged event shape");
@@ -216,7 +236,16 @@ function createBundleEventRow(input) {
216
236
  to_release_id: input.toReleaseId,
217
237
  user_id: input.userId ?? null
218
238
  };
239
+ const response = input.metadata?.httpResponse;
219
240
  const metadata = {
241
+ ...response ? { http_response: {
242
+ resource: response.resource,
243
+ path: response.path,
244
+ status: response.status,
245
+ body: response.body,
246
+ body_truncated: response.bodyTruncated,
247
+ received_at_ms: response.receivedAtMs
248
+ } } : {},
220
249
  cohort: input.cohort,
221
250
  fingerprint_hash: input.fingerprintHash,
222
251
  sdk_version: input.sdkVersion ?? null,
@@ -98,6 +98,17 @@ function readFailure(value) {
98
98
  ...previousProcessExit === void 0 ? {} : { previousProcessExit }
99
99
  };
100
100
  }
101
+ function readHttpResponse(value) {
102
+ if (!isRecord(value) || value.resource !== "catalog" && value.resource !== "artifact" || typeof value.path !== "string" || value.path.length > 1024 || !Number.isSafeInteger(value.status) || value.status < 100 || value.status > 599 || value.body !== null && (typeof value.body !== "string" || new TextEncoder().encode(JSON.stringify(value.body)).byteLength > 4096) || typeof value.bodyTruncated !== "boolean" || !Number.isSafeInteger(value.receivedAtMs) || value.receivedAtMs < 0) throw new InsightsBadRequestError("Invalid HTTP response details");
103
+ return {
104
+ resource: value.resource,
105
+ path: value.path,
106
+ status: value.status,
107
+ body: value.body,
108
+ bodyTruncated: value.bodyTruncated,
109
+ receivedAtMs: value.receivedAtMs
110
+ };
111
+ }
101
112
  async function readBoundedText(request) {
102
113
  const contentLength = request.headers.get("content-length");
103
114
  const declaredByteLength = Number(contentLength);
@@ -133,7 +144,10 @@ function requireEvent(payload) {
133
144
  const platform = requireStringField(payload, "platform");
134
145
  if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
135
146
  const eventId = readEventId(payload);
147
+ const metadata = readMetadata(payload);
148
+ const httpMetadata = metadata.httpResponse === void 0 ? {} : { httpResponse: readHttpResponse(metadata.httpResponse) };
136
149
  const base = {
150
+ ...metadata.httpResponse === void 0 ? {} : { metadata: httpMetadata },
137
151
  ...eventId === void 0 ? {} : { eventId },
138
152
  installId: requireIdentityField(payload, "installId"),
139
153
  toBundleId: requireStringField(payload, "toBundleId"),
@@ -157,11 +171,11 @@ function requireEvent(payload) {
157
171
  updateStrategy
158
172
  };
159
173
  };
160
- const metadata = readMetadata(payload);
161
174
  switch (type) {
162
175
  case "UPDATE_DOWNLOADED": {
163
176
  const delivery = metadata.delivery;
164
177
  const read = {
178
+ ...httpMetadata,
165
179
  ...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
166
180
  ...metadata.patchFallback === true ? { patchFallback: true } : {}
167
181
  };
@@ -180,13 +194,19 @@ function requireEvent(payload) {
180
194
  return {
181
195
  ...movement(),
182
196
  type,
183
- ...previousProcessExit === void 0 ? {} : { metadata: { previousProcessExit } }
197
+ ...previousProcessExit === void 0 ? {} : { metadata: {
198
+ ...httpMetadata,
199
+ previousProcessExit
200
+ } }
184
201
  };
185
202
  }
186
203
  case "UPDATE_FAILED": return {
187
204
  ...movement(),
188
205
  type,
189
- metadata: { failure: readFailure(metadata.failure) }
206
+ metadata: {
207
+ ...httpMetadata,
208
+ failure: readFailure(metadata.failure)
209
+ }
190
210
  };
191
211
  case "UNCHANGED":
192
212
  if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
@@ -216,7 +236,16 @@ function createBundleEventRow(input) {
216
236
  to_release_id: input.toReleaseId,
217
237
  user_id: input.userId ?? null
218
238
  };
239
+ const response = input.metadata?.httpResponse;
219
240
  const metadata = {
241
+ ...response ? { http_response: {
242
+ resource: response.resource,
243
+ path: response.path,
244
+ status: response.status,
245
+ body: response.body,
246
+ body_truncated: response.bodyTruncated,
247
+ received_at_ms: response.receivedAtMs
248
+ } } : {},
220
249
  cohort: input.cohort,
221
250
  fingerprint_hash: input.fingerprintHash,
222
251
  sdk_version: input.sdkVersion ?? null,
@@ -2,9 +2,10 @@ let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
2
2
  //#region src/server/eventRow.ts
3
3
  const isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
4
4
  const isString = (value) => typeof value === "string";
5
+ const isDatabaseHttpResponse = (value) => (0, _hot_updater_plugin_core.isDatabaseJsonObject)(value) && (value.resource === "catalog" || value.resource === "artifact") && isString(value.path) && Number.isSafeInteger(value.status) && (value.body === null || isString(value.body)) && typeof value.body_truncated === "boolean" && Number.isSafeInteger(value.received_at_ms);
5
6
  /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
6
7
  const isDatabaseBundleEventFailure = (value) => (0, _hot_updater_plugin_core.isDatabaseJsonObject)(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "error_message", isString) && isOptional(value, "error_stack", isString) && isOptional(value, "previous_process_exit", isString);
7
- const isDatabaseBundleEventMetadata = (value) => (0, _hot_updater_plugin_core.isDatabaseJsonObject)(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "previous_process_exit", isString);
8
+ const isDatabaseBundleEventMetadata = (value) => (0, _hot_updater_plugin_core.isDatabaseJsonObject)(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "http_response", isDatabaseHttpResponse) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "previous_process_exit", isString);
8
9
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
10
  const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
10
11
  const isTextOrNull = (value) => value === null || typeof value === "string";
@@ -19,6 +19,14 @@ type BundleEventFailure = DatabaseJsonObject & {
19
19
  /** Android's `ApplicationExitInfo` reason for the process before this one. */
20
20
  readonly previous_process_exit?: string;
21
21
  };
22
+ type DatabaseHttpResponse = DatabaseJsonObject & {
23
+ readonly resource: "catalog" | "artifact";
24
+ readonly path: string;
25
+ readonly status: number;
26
+ readonly body: string | null;
27
+ readonly body_truncated: boolean;
28
+ readonly received_at_ms: number;
29
+ };
22
30
  /** Ancillary report data; queryable identity and lifecycle fields stay on the row. */
23
31
  type DatabaseBundleEventMetadata = DatabaseJsonObject & {
24
32
  readonly cohort: string;
@@ -27,6 +35,7 @@ type DatabaseBundleEventMetadata = DatabaseJsonObject & {
27
35
  readonly sdk_version: string | null;
28
36
  /** `UPDATE_FAILED`: what failed. */
29
37
  readonly failure?: BundleEventFailure;
38
+ readonly http_response?: DatabaseHttpResponse;
30
39
  /** `UPDATE_DOWNLOADED`: how the bundle arrived. */
31
40
  readonly delivery?: "patch" | "manifest" | "archive" | "unknown";
32
41
  /** `UPDATE_DOWNLOADED`: a patch failed and the full files came instead. */
@@ -63,4 +72,4 @@ type BundleEventRow = BundleEventRowBase & ({
63
72
  readonly from_bundle_id: null;
64
73
  });
65
74
  //#endregion
66
- export { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, BundleEventRow, BundleEventRowBase, DatabaseBundleEventMetadata };
75
+ export { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, BundleEventRow, BundleEventRowBase, DatabaseBundleEventMetadata, DatabaseHttpResponse };
@@ -19,6 +19,14 @@ type BundleEventFailure = DatabaseJsonObject & {
19
19
  /** Android's `ApplicationExitInfo` reason for the process before this one. */
20
20
  readonly previous_process_exit?: string;
21
21
  };
22
+ type DatabaseHttpResponse = DatabaseJsonObject & {
23
+ readonly resource: "catalog" | "artifact";
24
+ readonly path: string;
25
+ readonly status: number;
26
+ readonly body: string | null;
27
+ readonly body_truncated: boolean;
28
+ readonly received_at_ms: number;
29
+ };
22
30
  /** Ancillary report data; queryable identity and lifecycle fields stay on the row. */
23
31
  type DatabaseBundleEventMetadata = DatabaseJsonObject & {
24
32
  readonly cohort: string;
@@ -27,6 +35,7 @@ type DatabaseBundleEventMetadata = DatabaseJsonObject & {
27
35
  readonly sdk_version: string | null;
28
36
  /** `UPDATE_FAILED`: what failed. */
29
37
  readonly failure?: BundleEventFailure;
38
+ readonly http_response?: DatabaseHttpResponse;
30
39
  /** `UPDATE_DOWNLOADED`: how the bundle arrived. */
31
40
  readonly delivery?: "patch" | "manifest" | "archive" | "unknown";
32
41
  /** `UPDATE_DOWNLOADED`: a patch failed and the full files came instead. */
@@ -63,4 +72,4 @@ type BundleEventRow = BundleEventRowBase & ({
63
72
  readonly from_bundle_id: null;
64
73
  });
65
74
  //#endregion
66
- export { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, BundleEventRow, BundleEventRowBase, DatabaseBundleEventMetadata };
75
+ export { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, BundleEventRow, BundleEventRowBase, DatabaseBundleEventMetadata, DatabaseHttpResponse };
@@ -2,9 +2,10 @@ import { DatabaseAdapterInputError, isDatabaseJsonObject } from "@hot-updater/pl
2
2
  //#region src/server/eventRow.ts
3
3
  const isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
4
4
  const isString = (value) => typeof value === "string";
5
+ const isDatabaseHttpResponse = (value) => isDatabaseJsonObject(value) && (value.resource === "catalog" || value.resource === "artifact") && isString(value.path) && Number.isSafeInteger(value.status) && (value.body === null || isString(value.body)) && typeof value.body_truncated === "boolean" && Number.isSafeInteger(value.received_at_ms);
5
6
  /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
6
7
  const isDatabaseBundleEventFailure = (value) => isDatabaseJsonObject(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "error_message", isString) && isOptional(value, "error_stack", isString) && isOptional(value, "previous_process_exit", isString);
7
- const isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "previous_process_exit", isString);
8
+ const isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "http_response", isDatabaseHttpResponse) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "previous_process_exit", isString);
8
9
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
10
  const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
10
11
  const isTextOrNull = (value) => value === null || typeof value === "string";
@@ -2,7 +2,7 @@ import { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage,
2
2
  import { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference } from "./modelTypes.cjs";
3
3
  import { DEFAULT_INSIGHTS_RETENTION, InsightsRetention, InsightsSchema, createInsightsSchema, insightsSchema } from "./schema.cjs";
4
4
  import { InsightsFailureBreakdown, InsightsUpdateFailures, InsightsUpdateFailuresInput } from "./reads.cjs";
5
- import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.cjs";
5
+ import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InsightsHttpResponse, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.cjs";
6
6
  import { InsightsBadRequestError } from "./errors.cjs";
7
7
  import { createInsightsModel } from "./model.cjs";
8
8
  import { InsightsEventPageInput, InsightsInstallationEventPageInput, InsightsProvider, InsightsUserInstallationPageInput } from "./types.cjs";
@@ -392,4 +392,4 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
392
392
  endpoints: PluginEndpoint[];
393
393
  }, {}>;
394
394
  //#endregion
395
- export { type ActiveInstallationWindow, type BundleEventFailure, type BundleEventFailureInput, type BundleEventFailureReason, type BundleEventFailureStage, type BundleEventRow, type BundleEventRowBase, type CreateBundleEventRequest, type CreateBundleEventRequestBase, type CursorPage, DEFAULT_INSIGHTS_RETENTION, type DatabaseBundleEventMetadata, type EventCursorPage, type EventHistoryRow, InsightsApi, InsightsBadRequestError, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsIdentityParts, type InsightsInstallationEventPageInput, type InsightsListEventsInput, type InsightsModel, InsightsOptions, type InsightsProvider, type InsightsRecordEventInput, type InsightsRetention, type InsightsSchema, type InsightsScope, type InsightsTimeRange, type InsightsUpdateFailures, type InsightsUpdateFailuresInput, type InsightsUserInstallationPageInput, type InstallationHistoryRow, type InstallationRow, type ReleaseActivityMetrics, type ReleaseReference, type ReportingOverview, createInsightsModel, createInsightsProvider, createInsightsSchema, insights, insightsIdentity, insightsSchema };
395
+ export { type ActiveInstallationWindow, type BundleEventFailure, type BundleEventFailureInput, type BundleEventFailureReason, type BundleEventFailureStage, type BundleEventRow, type BundleEventRowBase, type CreateBundleEventRequest, type CreateBundleEventRequestBase, type CursorPage, DEFAULT_INSIGHTS_RETENTION, type DatabaseBundleEventMetadata, type EventCursorPage, type EventHistoryRow, InsightsApi, InsightsBadRequestError, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsHttpResponse, type InsightsIdentityParts, type InsightsInstallationEventPageInput, type InsightsListEventsInput, type InsightsModel, InsightsOptions, type InsightsProvider, type InsightsRecordEventInput, type InsightsRetention, type InsightsSchema, type InsightsScope, type InsightsTimeRange, type InsightsUpdateFailures, type InsightsUpdateFailuresInput, type InsightsUserInstallationPageInput, type InstallationHistoryRow, type InstallationRow, type ReleaseActivityMetrics, type ReleaseReference, type ReportingOverview, createInsightsModel, createInsightsProvider, createInsightsSchema, insights, insightsIdentity, insightsSchema };
@@ -2,7 +2,7 @@ import { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage,
2
2
  import { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference } from "./modelTypes.mjs";
3
3
  import { DEFAULT_INSIGHTS_RETENTION, InsightsRetention, InsightsSchema, createInsightsSchema, insightsSchema } from "./schema.mjs";
4
4
  import { InsightsFailureBreakdown, InsightsUpdateFailures, InsightsUpdateFailuresInput } from "./reads.mjs";
5
- import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.mjs";
5
+ import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InsightsHttpResponse, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.mjs";
6
6
  import { InsightsBadRequestError } from "./errors.mjs";
7
7
  import { createInsightsModel } from "./model.mjs";
8
8
  import { InsightsEventPageInput, InsightsInstallationEventPageInput, InsightsProvider, InsightsUserInstallationPageInput } from "./types.mjs";
@@ -392,4 +392,4 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
392
392
  endpoints: PluginEndpoint[];
393
393
  }, {}>;
394
394
  //#endregion
395
- export { type ActiveInstallationWindow, type BundleEventFailure, type BundleEventFailureInput, type BundleEventFailureReason, type BundleEventFailureStage, type BundleEventRow, type BundleEventRowBase, type CreateBundleEventRequest, type CreateBundleEventRequestBase, type CursorPage, DEFAULT_INSIGHTS_RETENTION, type DatabaseBundleEventMetadata, type EventCursorPage, type EventHistoryRow, InsightsApi, InsightsBadRequestError, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsIdentityParts, type InsightsInstallationEventPageInput, type InsightsListEventsInput, type InsightsModel, InsightsOptions, type InsightsProvider, type InsightsRecordEventInput, type InsightsRetention, type InsightsSchema, type InsightsScope, type InsightsTimeRange, type InsightsUpdateFailures, type InsightsUpdateFailuresInput, type InsightsUserInstallationPageInput, type InstallationHistoryRow, type InstallationRow, type ReleaseActivityMetrics, type ReleaseReference, type ReportingOverview, createInsightsModel, createInsightsProvider, createInsightsSchema, insights, insightsIdentity, insightsSchema };
395
+ export { type ActiveInstallationWindow, type BundleEventFailure, type BundleEventFailureInput, type BundleEventFailureReason, type BundleEventFailureStage, type BundleEventRow, type BundleEventRowBase, type CreateBundleEventRequest, type CreateBundleEventRequestBase, type CursorPage, DEFAULT_INSIGHTS_RETENTION, type DatabaseBundleEventMetadata, type EventCursorPage, type EventHistoryRow, InsightsApi, InsightsBadRequestError, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsHttpResponse, type InsightsIdentityParts, type InsightsInstallationEventPageInput, type InsightsListEventsInput, type InsightsModel, InsightsOptions, type InsightsProvider, type InsightsRecordEventInput, type InsightsRetention, type InsightsSchema, type InsightsScope, type InsightsTimeRange, type InsightsUpdateFailures, type InsightsUpdateFailuresInput, type InsightsUserInstallationPageInput, type InstallationHistoryRow, type InstallationRow, type ReleaseActivityMetrics, type ReleaseReference, type ReportingOverview, createInsightsModel, createInsightsProvider, createInsightsSchema, insights, insightsIdentity, insightsSchema };
@@ -157,6 +157,14 @@ const toFailure = (failure) => ({
157
157
  });
158
158
  /** What a row's metadata adds for its type: a failure, a delivery, an exit reason. */
159
159
  const eventDetails = ({ type, metadata }) => ({
160
+ ...metadata.http_response !== void 0 ? { httpResponse: {
161
+ resource: metadata.http_response.resource,
162
+ path: metadata.http_response.path,
163
+ status: metadata.http_response.status,
164
+ body: metadata.http_response.body,
165
+ bodyTruncated: metadata.http_response.body_truncated,
166
+ receivedAtMs: metadata.http_response.received_at_ms
167
+ } } : {},
160
168
  ...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
161
169
  ...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
162
170
  ...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {},
@@ -179,6 +187,7 @@ const toEventHistoryRow = (row) => ({
179
187
  ...eventDetails(row)
180
188
  });
181
189
  const toInstallationRow = (row) => ({
190
+ ...eventDetails(row).httpResponse ? { httpResponse: eventDetails(row).httpResponse } : {},
182
191
  appVersion: row.app_version,
183
192
  channel: row.channel,
184
193
  cohort: row.metadata.cohort,
@@ -157,6 +157,14 @@ const toFailure = (failure) => ({
157
157
  });
158
158
  /** What a row's metadata adds for its type: a failure, a delivery, an exit reason. */
159
159
  const eventDetails = ({ type, metadata }) => ({
160
+ ...metadata.http_response !== void 0 ? { httpResponse: {
161
+ resource: metadata.http_response.resource,
162
+ path: metadata.http_response.path,
163
+ status: metadata.http_response.status,
164
+ body: metadata.http_response.body,
165
+ bodyTruncated: metadata.http_response.body_truncated,
166
+ receivedAtMs: metadata.http_response.received_at_ms
167
+ } } : {},
160
168
  ...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
161
169
  ...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
162
170
  ...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {},
@@ -179,6 +187,7 @@ const toEventHistoryRow = (row) => ({
179
187
  ...eventDetails(row)
180
188
  });
181
189
  const toInstallationRow = (row) => ({
190
+ ...eventDetails(row).httpResponse ? { httpResponse: eventDetails(row).httpResponse } : {},
182
191
  appVersion: row.app_version,
183
192
  channel: row.channel,
184
193
  cohort: row.metadata.cohort,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/plugin-insights",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.28",
4
+ "version": "1.0.0-rc.29",
5
5
  "description": "Hot Updater's Insights plugin: launch, download, and update failure events, for the server and the React Native app",
6
6
  "exports": {
7
7
  "./client": {
@@ -33,10 +33,10 @@
33
33
  "package.json"
34
34
  ],
35
35
  "dependencies": {
36
- "@hot-updater/protocol": "1.0.0-rc.27"
36
+ "@hot-updater/protocol": "1.0.0-rc.28"
37
37
  },
38
38
  "peerDependencies": {
39
- "@hot-updater/plugin-core": "^1.0.0-rc.27"
39
+ "@hot-updater/plugin-core": "^1.0.0-rc.28"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@hot-updater/plugin-core": {
@@ -46,8 +46,8 @@
46
46
  "devDependencies": {
47
47
  "@types/node": "^20",
48
48
  "vitest": "4.1.4",
49
- "@hot-updater/plugin-core": "1.0.0-rc.27",
50
- "@hot-updater/test-utils": "1.0.0-rc.27"
49
+ "@hot-updater/plugin-core": "1.0.0-rc.28",
50
+ "@hot-updater/test-utils": "1.0.0-rc.28"
51
51
  },
52
52
  "nx": {
53
53
  "implicitDependencies": [