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

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.
@@ -0,0 +1,34 @@
1
+ let _hot_updater_protocol = require("@hot-updater/protocol");
2
+ //#region src/client/errorDetails.ts
3
+ /** Bound the JSON wire size without rewriting the error text or splitting Unicode. */
4
+ const boundedText = (text, maxBytes) => {
5
+ if ((0, _hot_updater_protocol.getUtf8ByteLength)(JSON.stringify(text)) <= maxBytes) return text;
6
+ let result = "";
7
+ let bytes = 2 + (0, _hot_updater_protocol.getUtf8ByteLength)("…");
8
+ for (const character of text) {
9
+ bytes += (0, _hot_updater_protocol.getUtf8ByteLength)(JSON.stringify(character)) - 2;
10
+ if (bytes > maxBytes) break;
11
+ result += character;
12
+ }
13
+ return `${result}…`;
14
+ };
15
+ /** Error properties are not enumerable, so JSON.stringify(error) would lose them. */
16
+ const readErrorDetails = (error) => {
17
+ if (error == null) return {};
18
+ const object = typeof error === "object" ? error : null;
19
+ let message;
20
+ if (object && "message" in object && typeof object.message === "string") message = object.message;
21
+ else if (typeof error === "string") message = error;
22
+ else try {
23
+ message = JSON.stringify(error) ?? String(error);
24
+ } catch {
25
+ message = String(error);
26
+ }
27
+ const stack = object && "stack" in object && typeof object.stack === "string" ? object.stack : void 0;
28
+ return {
29
+ ...message ? { errorMessage: boundedText(message, 2048) } : {},
30
+ ...stack ? { errorStack: boundedText(stack, 4096) } : {}
31
+ };
32
+ };
33
+ //#endregion
34
+ exports.readErrorDetails = readErrorDetails;
@@ -0,0 +1,34 @@
1
+ import { getUtf8ByteLength } from "@hot-updater/protocol";
2
+ //#region src/client/errorDetails.ts
3
+ /** Bound the JSON wire size without rewriting the error text or splitting Unicode. */
4
+ const boundedText = (text, maxBytes) => {
5
+ if (getUtf8ByteLength(JSON.stringify(text)) <= maxBytes) return text;
6
+ let result = "";
7
+ let bytes = 2 + getUtf8ByteLength("…");
8
+ for (const character of text) {
9
+ bytes += getUtf8ByteLength(JSON.stringify(character)) - 2;
10
+ if (bytes > maxBytes) break;
11
+ result += character;
12
+ }
13
+ return `${result}…`;
14
+ };
15
+ /** Error properties are not enumerable, so JSON.stringify(error) would lose them. */
16
+ const readErrorDetails = (error) => {
17
+ if (error == null) return {};
18
+ const object = typeof error === "object" ? error : null;
19
+ let message;
20
+ if (object && "message" in object && typeof object.message === "string") message = object.message;
21
+ else if (typeof error === "string") message = error;
22
+ else try {
23
+ message = JSON.stringify(error) ?? String(error);
24
+ } catch {
25
+ message = String(error);
26
+ }
27
+ const stack = object && "stack" in object && typeof object.stack === "string" ? object.stack : void 0;
28
+ return {
29
+ ...message ? { errorMessage: boundedText(message, 2048) } : {},
30
+ ...stack ? { errorStack: boundedText(stack, 4096) } : {}
31
+ };
32
+ };
33
+ //#endregion
34
+ export { readErrorDetails };
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errorDetails = require("./errorDetails.cjs");
2
3
  const require_eventId = require("./eventId.cjs");
3
4
  const require_sender = require("./sender.cjs");
4
5
  const require_state = require("./state.cjs");
@@ -140,11 +141,13 @@ const insights = (options = {}) => {
140
141
  const onUpdateError = (error) => {
141
142
  if (error.stage === "check" && error.reason === "network") return;
142
143
  const targetBundleId = error.targetBundleId ?? error.bundleId;
143
- const failureKey = [
144
+ const details = require_errorDetails.readErrorDetails(error.cause);
145
+ const failureKey = JSON.stringify([
144
146
  error.stage,
145
147
  error.reason,
146
- targetBundleId
147
- ].join(" ");
148
+ targetBundleId,
149
+ details.errorMessage ?? null
150
+ ]);
148
151
  const event = createEvent({
149
152
  type: "UPDATE_FAILED",
150
153
  channel: error.channel,
@@ -156,6 +159,7 @@ const insights = (options = {}) => {
156
159
  metadata: { failure: {
157
160
  stage: error.stage,
158
161
  reason: error.reason,
162
+ ...details,
159
163
  ...error.resource === void 0 ? {} : { resource: error.resource },
160
164
  ...error.httpStatus === void 0 ? {} : { httpStatus: error.httpStatus },
161
165
  ...error.transport === void 0 ? {} : { transport: error.transport },
@@ -1,3 +1,4 @@
1
+ import { readErrorDetails } from "./errorDetails.mjs";
1
2
  import { createKeyedUUIDv7, createUUIDv7 } from "./eventId.mjs";
2
3
  import { createInsightsEventSender } from "./sender.mjs";
3
4
  import { createInsightsState, utcDay, utcDayStart } from "./state.mjs";
@@ -139,11 +140,13 @@ const insights = (options = {}) => {
139
140
  const onUpdateError = (error) => {
140
141
  if (error.stage === "check" && error.reason === "network") return;
141
142
  const targetBundleId = error.targetBundleId ?? error.bundleId;
142
- const failureKey = [
143
+ const details = readErrorDetails(error.cause);
144
+ const failureKey = JSON.stringify([
143
145
  error.stage,
144
146
  error.reason,
145
- targetBundleId
146
- ].join(" ");
147
+ targetBundleId,
148
+ details.errorMessage ?? null
149
+ ]);
147
150
  const event = createEvent({
148
151
  type: "UPDATE_FAILED",
149
152
  channel: error.channel,
@@ -155,6 +158,7 @@ const insights = (options = {}) => {
155
158
  metadata: { failure: {
156
159
  stage: error.stage,
157
160
  reason: error.reason,
161
+ ...details,
158
162
  ...error.resource === void 0 ? {} : { resource: error.resource },
159
163
  ...error.httpStatus === void 0 ? {} : { httpStatus: error.httpStatus },
160
164
  ...error.transport === void 0 ? {} : { transport: error.transport },
@@ -14,6 +14,9 @@ type BundleEventFailureInput = {
14
14
  readonly transport?: NonNullable<BundleEventFailure["transport"]>;
15
15
  /** The storage origin's error code, such as `ExpiredToken`. */
16
16
  readonly originCode?: string;
17
+ /** Original client error text, bounded to fit the event payload. */
18
+ readonly errorMessage?: string;
19
+ readonly errorStack?: string;
17
20
  /** Android's `ApplicationExitInfo` reason for the process before this one. */
18
21
  readonly previousProcessExit?: string;
19
22
  };
@@ -78,6 +81,8 @@ type CreateBundleEventRequest = (Movement & {
78
81
  });
79
82
  type ActiveInstallationWindow = "24h" | "7d" | "30d";
80
83
  type EventHistoryRow = {
84
+ readonly toReleaseId?: string;
85
+ readonly sdkVersion?: string;
81
86
  readonly id: string;
82
87
  readonly installId: string;
83
88
  readonly type: "UPDATE_DOWNLOADED" | "UPDATE_APPLIED" | "RECOVERED" | "UPDATE_FAILED" | "UNCHANGED";
@@ -14,6 +14,9 @@ type BundleEventFailureInput = {
14
14
  readonly transport?: NonNullable<BundleEventFailure["transport"]>;
15
15
  /** The storage origin's error code, such as `ExpiredToken`. */
16
16
  readonly originCode?: string;
17
+ /** Original client error text, bounded to fit the event payload. */
18
+ readonly errorMessage?: string;
19
+ readonly errorStack?: string;
17
20
  /** Android's `ApplicationExitInfo` reason for the process before this one. */
18
21
  readonly previousProcessExit?: string;
19
22
  };
@@ -78,6 +81,8 @@ type CreateBundleEventRequest = (Movement & {
78
81
  });
79
82
  type ActiveInstallationWindow = "24h" | "7d" | "30d";
80
83
  type EventHistoryRow = {
84
+ readonly toReleaseId?: string;
85
+ readonly sdkVersion?: string;
81
86
  readonly id: string;
82
87
  readonly installId: string;
83
88
  readonly type: "UPDATE_DOWNLOADED" | "UPDATE_APPLIED" | "RECOVERED" | "UPDATE_FAILED" | "UNCHANGED";
@@ -83,6 +83,8 @@ function readFailure(value) {
83
83
  const failure = isRecord(value) ? value : {};
84
84
  const httpStatus = failure.httpStatus;
85
85
  const originCode = readCode(failure.originCode);
86
+ const errorMessage = typeof failure.errorMessage === "string" && failure.errorMessage.length <= 2048 ? failure.errorMessage : void 0;
87
+ const errorStack = typeof failure.errorStack === "string" && failure.errorStack.length <= 4096 ? failure.errorStack : void 0;
86
88
  const previousProcessExit = readCode(failure.previousProcessExit);
87
89
  return {
88
90
  stage: oneOf(STAGES, failure.stage),
@@ -91,6 +93,8 @@ function readFailure(value) {
91
93
  ...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
92
94
  ...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
93
95
  ...originCode === void 0 ? {} : { originCode },
96
+ ...errorMessage === void 0 ? {} : { errorMessage },
97
+ ...errorStack === void 0 ? {} : { errorStack },
94
98
  ...previousProcessExit === void 0 ? {} : { previousProcessExit }
95
99
  };
96
100
  }
@@ -261,6 +265,8 @@ function createBundleEventRow(input) {
261
265
  failure: {
262
266
  stage: failure.stage,
263
267
  reason: failure.reason,
268
+ ...failure.errorMessage === void 0 ? {} : { error_message: failure.errorMessage },
269
+ ...failure.errorStack === void 0 ? {} : { error_stack: failure.errorStack },
264
270
  ...failure.resource === void 0 ? {} : { resource: failure.resource },
265
271
  ...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
266
272
  ...failure.transport === void 0 ? {} : { transport: failure.transport },
@@ -83,6 +83,8 @@ function readFailure(value) {
83
83
  const failure = isRecord(value) ? value : {};
84
84
  const httpStatus = failure.httpStatus;
85
85
  const originCode = readCode(failure.originCode);
86
+ const errorMessage = typeof failure.errorMessage === "string" && failure.errorMessage.length <= 2048 ? failure.errorMessage : void 0;
87
+ const errorStack = typeof failure.errorStack === "string" && failure.errorStack.length <= 4096 ? failure.errorStack : void 0;
86
88
  const previousProcessExit = readCode(failure.previousProcessExit);
87
89
  return {
88
90
  stage: oneOf(STAGES, failure.stage),
@@ -91,6 +93,8 @@ function readFailure(value) {
91
93
  ...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
92
94
  ...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
93
95
  ...originCode === void 0 ? {} : { originCode },
96
+ ...errorMessage === void 0 ? {} : { errorMessage },
97
+ ...errorStack === void 0 ? {} : { errorStack },
94
98
  ...previousProcessExit === void 0 ? {} : { previousProcessExit }
95
99
  };
96
100
  }
@@ -261,6 +265,8 @@ function createBundleEventRow(input) {
261
265
  failure: {
262
266
  stage: failure.stage,
263
267
  reason: failure.reason,
268
+ ...failure.errorMessage === void 0 ? {} : { error_message: failure.errorMessage },
269
+ ...failure.errorStack === void 0 ? {} : { error_stack: failure.errorStack },
264
270
  ...failure.resource === void 0 ? {} : { resource: failure.resource },
265
271
  ...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
266
272
  ...failure.transport === void 0 ? {} : { transport: failure.transport },
@@ -3,7 +3,7 @@ let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
3
3
  const isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
4
4
  const isString = (value) => typeof value === "string";
5
5
  /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
6
- 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, "previous_process_exit", isString);
6
+ 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
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
8
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
9
  const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
@@ -14,6 +14,8 @@ type BundleEventFailure = DatabaseJsonObject & {
14
14
  readonly transport?: "timeout" | "dns" | "tls" | "connection" | "offline" | "cancelled" | "unknown";
15
15
  /** The storage origin's error code, such as `ExpiredToken`. */
16
16
  readonly origin_code?: string;
17
+ readonly error_message?: string;
18
+ readonly error_stack?: string;
17
19
  /** Android's `ApplicationExitInfo` reason for the process before this one. */
18
20
  readonly previous_process_exit?: string;
19
21
  };
@@ -14,6 +14,8 @@ type BundleEventFailure = DatabaseJsonObject & {
14
14
  readonly transport?: "timeout" | "dns" | "tls" | "connection" | "offline" | "cancelled" | "unknown";
15
15
  /** The storage origin's error code, such as `ExpiredToken`. */
16
16
  readonly origin_code?: string;
17
+ readonly error_message?: string;
18
+ readonly error_stack?: string;
17
19
  /** Android's `ApplicationExitInfo` reason for the process before this one. */
18
20
  readonly previous_process_exit?: string;
19
21
  };
@@ -3,7 +3,7 @@ import { DatabaseAdapterInputError, isDatabaseJsonObject } from "@hot-updater/pl
3
3
  const isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
4
4
  const isString = (value) => typeof value === "string";
5
5
  /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
6
- 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, "previous_process_exit", isString);
6
+ 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
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
8
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
9
  const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
@@ -147,6 +147,8 @@ const assertEventRows = (rows, input) => {
147
147
  const toFailure = (failure) => ({
148
148
  stage: failure.stage,
149
149
  reason: failure.reason,
150
+ ...failure.error_message === void 0 ? {} : { errorMessage: failure.error_message },
151
+ ...failure.error_stack === void 0 ? {} : { errorStack: failure.error_stack },
150
152
  ...failure.resource === void 0 ? {} : { resource: failure.resource },
151
153
  ...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
152
154
  ...failure.transport === void 0 ? {} : { transport: failure.transport },
@@ -161,6 +163,8 @@ const eventDetails = ({ type, metadata }) => ({
161
163
  ...type === "RECOVERED" && metadata.previous_process_exit !== void 0 ? { previousProcessExit: metadata.previous_process_exit } : {}
162
164
  });
163
165
  const toEventHistoryRow = (row) => ({
166
+ ...row.to_release_id == null ? {} : { toReleaseId: row.to_release_id },
167
+ ...row.metadata.sdk_version == null ? {} : { sdkVersion: row.metadata.sdk_version },
164
168
  appVersion: row.app_version,
165
169
  channel: row.channel,
166
170
  cohort: row.metadata.cohort,
@@ -147,6 +147,8 @@ const assertEventRows = (rows, input) => {
147
147
  const toFailure = (failure) => ({
148
148
  stage: failure.stage,
149
149
  reason: failure.reason,
150
+ ...failure.error_message === void 0 ? {} : { errorMessage: failure.error_message },
151
+ ...failure.error_stack === void 0 ? {} : { errorStack: failure.error_stack },
150
152
  ...failure.resource === void 0 ? {} : { resource: failure.resource },
151
153
  ...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
152
154
  ...failure.transport === void 0 ? {} : { transport: failure.transport },
@@ -161,6 +163,8 @@ const eventDetails = ({ type, metadata }) => ({
161
163
  ...type === "RECOVERED" && metadata.previous_process_exit !== void 0 ? { previousProcessExit: metadata.previous_process_exit } : {}
162
164
  });
163
165
  const toEventHistoryRow = (row) => ({
166
+ ...row.to_release_id == null ? {} : { toReleaseId: row.to_release_id },
167
+ ...row.metadata.sdk_version == null ? {} : { sdkVersion: row.metadata.sdk_version },
164
168
  appVersion: row.app_version,
165
169
  channel: row.channel,
166
170
  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.27",
4
+ "version": "1.0.0-rc.28",
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": {