@hot-updater/plugin-insights 1.0.0-rc.28 → 1.0.0-rc.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/errorDetails.cjs +1 -0
- package/dist/client/errorDetails.mjs +1 -1
- package/dist/client/index.cjs +18 -1
- package/dist/client/index.mjs +19 -2
- package/dist/client/state.cjs +18 -0
- package/dist/client/state.mjs +18 -0
- package/dist/server/contract.cjs +24 -3
- package/dist/server/contract.mjs +24 -3
- package/dist/server/domain.d.cts +10 -1
- package/dist/server/domain.d.mts +10 -1
- package/dist/server/eventInput.cjs +32 -3
- package/dist/server/eventInput.mjs +32 -3
- package/dist/server/eventRow.cjs +2 -1
- package/dist/server/eventRow.d.cts +10 -1
- package/dist/server/eventRow.d.mts +10 -1
- package/dist/server/eventRow.mjs +2 -1
- package/dist/server/index.cjs +28 -1
- package/dist/server/index.d.cts +67 -3
- package/dist/server/index.d.mts +67 -3
- package/dist/server/index.mjs +29 -2
- package/dist/server/model.cjs +2 -1
- package/dist/server/model.mjs +2 -1
- package/dist/server/modelTypes.d.cts +32 -2
- package/dist/server/modelTypes.d.mts +32 -2
- package/dist/server/provider.cjs +9 -0
- package/dist/server/provider.mjs +9 -0
- package/dist/server/reads.cjs +55 -6
- package/dist/server/reads.mjs +55 -7
- package/dist/server/recordEvent.cjs +50 -4
- package/dist/server/recordEvent.mjs +50 -4
- package/dist/server/schema.cjs +67 -0
- package/dist/server/schema.d.cts +124 -0
- package/dist/server/schema.d.mts +124 -0
- package/dist/server/schema.mjs +67 -0
- package/package.json +5 -5
package/dist/client/index.cjs
CHANGED
|
@@ -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) {
|
package/dist/client/index.mjs
CHANGED
|
@@ -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) {
|
package/dist/client/state.cjs
CHANGED
|
@@ -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");
|
package/dist/client/state.mjs
CHANGED
|
@@ -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");
|
package/dist/server/contract.cjs
CHANGED
|
@@ -154,19 +154,40 @@ const createValidatedInsightsModel = (model) => ({
|
|
|
154
154
|
if (!require_eventRow.isRecord(input) || !hasOnlyKeys(input, [
|
|
155
155
|
"releases",
|
|
156
156
|
"scope",
|
|
157
|
-
"timeRange"
|
|
157
|
+
"timeRange",
|
|
158
|
+
"intervalMs"
|
|
158
159
|
])) invalidQuery();
|
|
159
160
|
const releases = input.releases;
|
|
160
161
|
const scope = input.scope;
|
|
161
162
|
const timeRange = input.timeRange;
|
|
162
|
-
const
|
|
163
|
-
const
|
|
163
|
+
const intervalMs = input.intervalMs;
|
|
164
|
+
const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 ? intervalMs === void 0 : isTimeRange(timeRange) && (intervalMs === void 0 || isTimestamp(intervalMs) && intervalMs > 0 && intervalMs % 36e5 === 0 && (timeRange.end - timeRange.start) / intervalMs <= 8784));
|
|
165
|
+
const scopeQuery = releases === void 0 && intervalMs === void 0 && require_eventRow.isRecord(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
|
|
164
166
|
if (!releaseQuery && !scopeQuery) invalidQuery();
|
|
165
167
|
const result = await model.getReleaseActivity(input);
|
|
166
168
|
validateAggregateResult(result);
|
|
167
169
|
if (!Array.isArray(result.data)) invalidResult();
|
|
168
170
|
return result;
|
|
169
171
|
},
|
|
172
|
+
async getDistributionHistory(input) {
|
|
173
|
+
if (!require_eventRow.isRecord(input) || !hasOnlyKeys(input, [
|
|
174
|
+
"channel",
|
|
175
|
+
"platform",
|
|
176
|
+
"timeRange"
|
|
177
|
+
]) || !hasScope(input) || !isTimeRange(input.timeRange) || input.timeRange.start % 864e5 !== 0 || input.timeRange.end % 864e5 !== 0 || input.timeRange.end - input.timeRange.start > 26784e5) invalidQuery();
|
|
178
|
+
const result = await model.getDistributionHistory(input);
|
|
179
|
+
validateAggregateResult(result);
|
|
180
|
+
if (!Array.isArray(result.points)) invalidResult();
|
|
181
|
+
for (const point of result.points) {
|
|
182
|
+
if (!isTimestamp(point.startMs) || !Array.isArray(point.bundles)) invalidResult();
|
|
183
|
+
for (const bundle of point.bundles) if (!isText(bundle.appVersion) || bundle.releaseId !== null && !isText(bundle.releaseId) || ![
|
|
184
|
+
"release",
|
|
185
|
+
"builtin",
|
|
186
|
+
"unknown"
|
|
187
|
+
].includes(bundle.bundleKind) || !isTimestamp(bundle.installations)) invalidResult();
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
},
|
|
170
191
|
async getAppUsage(input) {
|
|
171
192
|
if (!require_eventRow.isRecord(input) || !hasOnlyKeys(input, [
|
|
172
193
|
"channel",
|
package/dist/server/contract.mjs
CHANGED
|
@@ -154,19 +154,40 @@ const createValidatedInsightsModel = (model) => ({
|
|
|
154
154
|
if (!isRecord(input) || !hasOnlyKeys(input, [
|
|
155
155
|
"releases",
|
|
156
156
|
"scope",
|
|
157
|
-
"timeRange"
|
|
157
|
+
"timeRange",
|
|
158
|
+
"intervalMs"
|
|
158
159
|
])) invalidQuery();
|
|
159
160
|
const releases = input.releases;
|
|
160
161
|
const scope = input.scope;
|
|
161
162
|
const timeRange = input.timeRange;
|
|
162
|
-
const
|
|
163
|
-
const
|
|
163
|
+
const intervalMs = input.intervalMs;
|
|
164
|
+
const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 ? intervalMs === void 0 : isTimeRange(timeRange) && (intervalMs === void 0 || isTimestamp(intervalMs) && intervalMs > 0 && intervalMs % 36e5 === 0 && (timeRange.end - timeRange.start) / intervalMs <= 8784));
|
|
165
|
+
const scopeQuery = releases === void 0 && intervalMs === void 0 && isRecord(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
|
|
164
166
|
if (!releaseQuery && !scopeQuery) invalidQuery();
|
|
165
167
|
const result = await model.getReleaseActivity(input);
|
|
166
168
|
validateAggregateResult(result);
|
|
167
169
|
if (!Array.isArray(result.data)) invalidResult();
|
|
168
170
|
return result;
|
|
169
171
|
},
|
|
172
|
+
async getDistributionHistory(input) {
|
|
173
|
+
if (!isRecord(input) || !hasOnlyKeys(input, [
|
|
174
|
+
"channel",
|
|
175
|
+
"platform",
|
|
176
|
+
"timeRange"
|
|
177
|
+
]) || !hasScope(input) || !isTimeRange(input.timeRange) || input.timeRange.start % 864e5 !== 0 || input.timeRange.end % 864e5 !== 0 || input.timeRange.end - input.timeRange.start > 26784e5) invalidQuery();
|
|
178
|
+
const result = await model.getDistributionHistory(input);
|
|
179
|
+
validateAggregateResult(result);
|
|
180
|
+
if (!Array.isArray(result.points)) invalidResult();
|
|
181
|
+
for (const point of result.points) {
|
|
182
|
+
if (!isTimestamp(point.startMs) || !Array.isArray(point.bundles)) invalidResult();
|
|
183
|
+
for (const bundle of point.bundles) if (!isText(bundle.appVersion) || bundle.releaseId !== null && !isText(bundle.releaseId) || ![
|
|
184
|
+
"release",
|
|
185
|
+
"builtin",
|
|
186
|
+
"unknown"
|
|
187
|
+
].includes(bundle.bundleKind) || !isTimestamp(bundle.installations)) invalidResult();
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
},
|
|
170
191
|
async getAppUsage(input) {
|
|
171
192
|
if (!isRecord(input) || !hasOnlyKeys(input, [
|
|
172
193
|
"channel",
|
package/dist/server/domain.d.cts
CHANGED
|
@@ -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 };
|
package/dist/server/domain.d.mts
CHANGED
|
@@ -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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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,
|
package/dist/server/eventRow.cjs
CHANGED
|
@@ -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 };
|
package/dist/server/eventRow.mjs
CHANGED
|
@@ -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";
|
package/dist/server/index.cjs
CHANGED
|
@@ -19,6 +19,7 @@ const createInsightsApi = (db, now, retention) => ({
|
|
|
19
19
|
countLatestEvents: (input) => require_reads.countLatestEvents(db, input),
|
|
20
20
|
countEvents: (input) => require_reads.countEvents(db, input),
|
|
21
21
|
getReleaseActivity: (input) => require_reads.getReleaseActivity(db, input, now, retention),
|
|
22
|
+
getDistributionHistory: (input) => require_reads.getDistributionHistory(db, input, now, retention),
|
|
22
23
|
getAppUsage: (input) => require_reads.getAppUsage(db, input, now, retention),
|
|
23
24
|
/** A release's or a channel's update failures, and their breakdown over a time range. */
|
|
24
25
|
getUpdateFailures: (input) => require_reads.getUpdateFailures(db, input, now, retention)
|
|
@@ -87,7 +88,7 @@ const insights = (options = {}) => {
|
|
|
87
88
|
const plugin = (0, _hot_updater_plugin_core.definePlugin)({
|
|
88
89
|
id: "insights",
|
|
89
90
|
namespace: false,
|
|
90
|
-
schemaVersion: "1.
|
|
91
|
+
schemaVersion: "1.3.0",
|
|
91
92
|
schema: require_schema.createInsightsSchema(retention),
|
|
92
93
|
init: ({ db, now }) => {
|
|
93
94
|
const api = createInsightsApi(db, now, retention);
|
|
@@ -102,6 +103,32 @@ const insights = (options = {}) => {
|
|
|
102
103
|
handler: (request, params) => routes[handler](params, request)
|
|
103
104
|
})),
|
|
104
105
|
failuresEndpoint(api),
|
|
106
|
+
{
|
|
107
|
+
access: "admin",
|
|
108
|
+
method: "GET",
|
|
109
|
+
path: "/distribution-history",
|
|
110
|
+
handler: async (request) => {
|
|
111
|
+
const query = new URL(request.url).searchParams;
|
|
112
|
+
try {
|
|
113
|
+
const result = await require_model.createInsightsModel(api).getDistributionHistory({
|
|
114
|
+
platform: query.get("platform"),
|
|
115
|
+
channel: query.get("channel") ?? "",
|
|
116
|
+
timeRange: {
|
|
117
|
+
start: Number(query.get("start") ?? NaN),
|
|
118
|
+
end: Number(query.get("end") ?? NaN)
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return Response.json(result, { headers: { "cache-control": "private, no-store" } });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof _hot_updater_plugin_core.DatabaseAdapterInputError) return Response.json({ error: "Choose a platform, channel, and up to 31 whole UTC days." }, { status: 400 });
|
|
124
|
+
if (!(0, _hot_updater_plugin_core.isDatabaseBusyError)(error)) throw error;
|
|
125
|
+
return Response.json({ error: "Service unavailable" }, {
|
|
126
|
+
status: 503,
|
|
127
|
+
headers: { "retry-after": "5" }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
105
132
|
{
|
|
106
133
|
access: "admin",
|
|
107
134
|
method: "GET",
|