gamealgo-cli 0.3.7 → 0.3.8
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.js +13 -5
- package/dist/commands/events.js +12 -2
- package/dist/commands/experiment.js +7 -3
- package/dist/commands/report.js +70 -16
- package/dist/event-catalog-diff.js +113 -0
- package/dist/http.js +30 -1
- package/dist/main.js +2 -1
- package/dist/output.js +10 -0
- package/dist/report-summary.js +300 -0
- package/dist/utils.js +6 -0
- package/package.json +1 -1
- package/skills/gamealgo/SKILL.md +28 -1
package/dist/client.js
CHANGED
|
@@ -58,10 +58,11 @@ export class GameAlgoAdminClient {
|
|
|
58
58
|
async archiveExperimentV2Strategy(strategyKey, archived) {
|
|
59
59
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/archive`, { archived });
|
|
60
60
|
}
|
|
61
|
-
async rollbackExperimentV2Strategy(strategyKey, versionId, message) {
|
|
61
|
+
async rollbackExperimentV2Strategy(strategyKey, versionId, message, platform) {
|
|
62
62
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/strategies-v2/${encodeURIComponent(strategyKey)}/rollback`, {
|
|
63
63
|
versionId,
|
|
64
64
|
message,
|
|
65
|
+
...(platform ? { platform } : {}),
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
async createExperimentV2Run(strategyKey, body) {
|
|
@@ -151,14 +152,21 @@ export class GameAlgoAdminClient {
|
|
|
151
152
|
async getReportPack() {
|
|
152
153
|
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack`);
|
|
153
154
|
}
|
|
154
|
-
async previewReportPack(content) {
|
|
155
|
-
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack/preview`, {
|
|
155
|
+
async previewReportPack(content, options = {}) {
|
|
156
|
+
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack/preview`, {
|
|
157
|
+
content,
|
|
158
|
+
...(options.responseMode ? { responseMode: options.responseMode } : {}),
|
|
159
|
+
});
|
|
156
160
|
}
|
|
157
161
|
async putReportPack(content) {
|
|
158
162
|
return await this.put(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/report-pack`, { content });
|
|
159
163
|
}
|
|
160
|
-
async reportManifest() {
|
|
161
|
-
|
|
164
|
+
async reportManifest(options = {}) {
|
|
165
|
+
const params = new URLSearchParams();
|
|
166
|
+
if (options.responseMode)
|
|
167
|
+
params.set("responseMode", options.responseMode);
|
|
168
|
+
const query = params.size ? `?${params.toString()}` : "";
|
|
169
|
+
return await this.get(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/manifest${query}`);
|
|
162
170
|
}
|
|
163
171
|
async queryReportDashboard(body, options = {}) {
|
|
164
172
|
return await this.post(`/admin/v1/games/${encodeURIComponent(await this.gameId())}/reports/query`, body, options);
|
package/dist/commands/events.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import YAML from "yaml";
|
|
2
2
|
import { parseFlags } from "../args.js";
|
|
3
|
+
import { diffReportPackEventCatalog } from "../event-catalog-diff.js";
|
|
3
4
|
import { confirm, formatDuration, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
|
|
4
|
-
import { readObjectFile, writeTextFile } from "../resources.js";
|
|
5
|
+
import { readJsonFile, readObjectFile, writeTextFile } from "../resources.js";
|
|
5
6
|
import { announceTargetGame } from "../target.js";
|
|
6
7
|
import { optionalString } from "../utils.js";
|
|
7
8
|
export async function handleEvents(client, args, global) {
|
|
8
9
|
const sub = args.shift();
|
|
9
10
|
const flags = parseFlags(args);
|
|
11
|
+
if (sub === "diff") {
|
|
12
|
+
const filePath = optionalString(flags.pack) || args.shift();
|
|
13
|
+
if (!filePath || args.length)
|
|
14
|
+
throw new Error("usage: gamealgo events diff --pack <report-pack.json>");
|
|
15
|
+
const reportPack = await readJsonFile(filePath);
|
|
16
|
+
const catalog = await client.listEventCatalog(30, false);
|
|
17
|
+
await printResult(diffReportPackEventCatalog(reportPack, catalog), global);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
10
20
|
if (sub === "list") {
|
|
11
21
|
if (args.length)
|
|
12
22
|
throw new Error("usage: gamealgo events list [--days 30] [--no-diagnostics]");
|
|
@@ -66,7 +76,7 @@ export async function handleEvents(client, args, global) {
|
|
|
66
76
|
return;
|
|
67
77
|
}
|
|
68
78
|
if (sub !== "count")
|
|
69
|
-
throw new Error("usage: gamealgo events <list|get|apply|deprecate|block|activate|count>");
|
|
79
|
+
throw new Error("usage: gamealgo events <list|get|diff|apply|deprecate|block|activate|count>");
|
|
70
80
|
const timeoutMs = reportTimeoutMs(flags);
|
|
71
81
|
const startedAt = Date.now();
|
|
72
82
|
const stopProgress = startProgress("Querying event counts", timeoutMs);
|
|
@@ -2,7 +2,7 @@ import YAML from "yaml";
|
|
|
2
2
|
import { parseFlags } from "../args.js";
|
|
3
3
|
import { confirm, isInteractive, optionalNonNegativeInteger, positiveInteger, printResult } from "../output.js";
|
|
4
4
|
import { objectValue, printExperimentRunVariantScripts, readObjectFile, validateExperimentRunVariantScripts, writeTextFile } from "../resources.js";
|
|
5
|
-
import { optionalString } from "../utils.js";
|
|
5
|
+
import { experimentPlatform, optionalString } from "../utils.js";
|
|
6
6
|
export async function handleExperiment(client, args, global) {
|
|
7
7
|
const sub = args.shift();
|
|
8
8
|
if (sub === "strategies" || sub === "list") {
|
|
@@ -126,12 +126,13 @@ export async function handleExperimentStrategy(client, args, global) {
|
|
|
126
126
|
const flags = parseFlags(args);
|
|
127
127
|
const strategyKey = optionalString(flags.strategy || flags.key) || optionalString(args.shift());
|
|
128
128
|
const versionId = optionalString(flags.version || flags["version-id"]);
|
|
129
|
+
const platform = flags.platform === undefined ? undefined : experimentPlatform(flags.platform);
|
|
129
130
|
const message = requiredMessage(flags, "experiment strategy rollback");
|
|
130
131
|
if (!strategyKey || !versionId) {
|
|
131
|
-
throw new Error("usage: gamealgo experiment strategy rollback <strategyKey> --version-id <versionId> --message <reason> --yes");
|
|
132
|
+
throw new Error("usage: gamealgo experiment strategy rollback <strategyKey> --version-id <versionId> [--platform ios|android|rest] --message <reason> --yes");
|
|
132
133
|
}
|
|
133
134
|
await requireYes(flags, global, "experiment strategy rollback");
|
|
134
|
-
await printResult(await client.rollbackExperimentV2Strategy(strategyKey, versionId, message), global);
|
|
135
|
+
await printResult(await client.rollbackExperimentV2Strategy(strategyKey, versionId, message, platform), global);
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
138
|
throw new Error("usage: gamealgo experiment strategy <show|publish|default|archive|restore|rollback>");
|
|
@@ -219,11 +220,14 @@ export async function handleExperimentRun(client, args, global) {
|
|
|
219
220
|
const run = response.run;
|
|
220
221
|
if (!run)
|
|
221
222
|
throw new Error(`experiment run not found: ${runId}`);
|
|
223
|
+
const reportConfig = objectValue(run.reportConfig);
|
|
224
|
+
const managedConfig = objectValue(reportConfig?.managed);
|
|
222
225
|
await printResult({
|
|
223
226
|
run: {
|
|
224
227
|
id: run.id,
|
|
225
228
|
strategyKey: run.strategyKey,
|
|
226
229
|
displayName: run.displayName,
|
|
230
|
+
platform: optionalString(reportConfig?.platform) ?? optionalString(managedConfig?.platform) ?? null,
|
|
227
231
|
type: run.type,
|
|
228
232
|
status: run.status,
|
|
229
233
|
objective: run.objective,
|
package/dist/commands/report.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { extname, join } from "node:path";
|
|
2
2
|
import { parseFlags, parseSelectorFlags } from "../args.js";
|
|
3
|
-
import { formatDuration, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
|
|
3
|
+
import { formatDuration, markCommandFailed, printResult, reportTimeoutMs, startProgress, withCliMeta } from "../output.js";
|
|
4
4
|
import { reportDashboardCsv, reportDashboardCsvExports } from "../report-csv.js";
|
|
5
|
+
import { normalizeReportSampleRows, summarizeReportDashboard, summarizeReportManifest, summarizeReportValidation, } from "../report-summary.js";
|
|
5
6
|
import { objectValue, readJsonFile, writeTextFile } from "../resources.js";
|
|
6
7
|
import { announceTargetGame } from "../target.js";
|
|
7
8
|
import { optionalString } from "../utils.js";
|
|
@@ -18,9 +19,15 @@ export async function handleReport(client, args, global) {
|
|
|
18
19
|
if (sub === "validate") {
|
|
19
20
|
const filePath = args.shift();
|
|
20
21
|
if (!filePath)
|
|
21
|
-
throw new Error("usage: gamealgo report validate <report-pack.json>");
|
|
22
|
+
throw new Error("usage: gamealgo report validate <report-pack.json> [--full]");
|
|
23
|
+
const flags = parseFlags(args);
|
|
24
|
+
const full = reportFullOutput(flags);
|
|
22
25
|
const content = await readJsonFile(filePath);
|
|
23
|
-
|
|
26
|
+
const response = await client.previewReportPack(content, { responseMode: full ? "full" : "summary" });
|
|
27
|
+
const summary = summarizeReportValidation(response);
|
|
28
|
+
await printResult(full ? response : summary, global);
|
|
29
|
+
if (!summary.ok)
|
|
30
|
+
markCommandFailed();
|
|
24
31
|
return;
|
|
25
32
|
}
|
|
26
33
|
if (sub === "publish") {
|
|
@@ -35,8 +42,10 @@ export async function handleReport(client, args, global) {
|
|
|
35
42
|
return;
|
|
36
43
|
}
|
|
37
44
|
if (sub === "manifest" || sub === "list") {
|
|
38
|
-
parseFlags(args);
|
|
39
|
-
|
|
45
|
+
const flags = parseFlags(args);
|
|
46
|
+
const full = reportFullOutput(flags);
|
|
47
|
+
const response = await client.reportManifest({ responseMode: full ? "full" : "summary" });
|
|
48
|
+
await printResult(full ? response : summarizeReportManifest(response), global);
|
|
40
49
|
return;
|
|
41
50
|
}
|
|
42
51
|
if (sub === "result") {
|
|
@@ -45,8 +54,12 @@ export async function handleReport(client, args, global) {
|
|
|
45
54
|
const startDate = String(flags.from || flags.start || flags.startDate || "");
|
|
46
55
|
const endDate = String(flags.to || flags.end || flags.endDate || "");
|
|
47
56
|
if (!startDate || !endDate)
|
|
48
|
-
throw new Error("usage: gamealgo report result --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--format json|csv] [--out file-or-directory]");
|
|
57
|
+
throw new Error("usage: gamealgo report result --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--sample-rows 0..20] [--full] [--format json|csv] [--out file-or-directory]");
|
|
58
|
+
requireReportQueryScope(flags, "report result");
|
|
49
59
|
const timeoutMs = reportTimeoutMs(flags);
|
|
60
|
+
const format = reportOutputFormat(flags.format);
|
|
61
|
+
const full = reportFullOutput(flags) || format === "csv";
|
|
62
|
+
const sampleRows = reportSampleRows(flags, full);
|
|
50
63
|
const platform = await resolveCliReportPlatform(client, flags.platform, selectors.platform, startDate, endDate);
|
|
51
64
|
const startedAt = Date.now();
|
|
52
65
|
const stopProgress = startProgress("Querying report results", timeoutMs);
|
|
@@ -64,15 +77,19 @@ export async function handleReport(client, args, global) {
|
|
|
64
77
|
platform,
|
|
65
78
|
selectors,
|
|
66
79
|
refresh: Boolean(flags.refresh),
|
|
80
|
+
responseMode: full ? "full" : "summary",
|
|
81
|
+
sampleRows,
|
|
67
82
|
}, { timeoutMs });
|
|
68
83
|
}
|
|
69
84
|
finally {
|
|
70
85
|
stopProgress();
|
|
71
86
|
}
|
|
72
87
|
const elapsedMs = Date.now() - startedAt;
|
|
73
|
-
const
|
|
88
|
+
const rawOutputValue = withCliMeta(response, { elapsedMs, timeoutMs });
|
|
89
|
+
const statusSummary = summarizeReportDashboard(rawOutputValue, { command: "report.result", sampleRows });
|
|
90
|
+
const outputValue = full ? rawOutputValue : statusSummary;
|
|
74
91
|
process.stderr.write(`Report query finished in ${formatDuration(elapsedMs)}.\n`);
|
|
75
|
-
await writeReportQueryOutput(outputValue, flags, global, elapsedMs);
|
|
92
|
+
await writeReportQueryOutput(outputValue, flags, global, elapsedMs, statusSummary.ok);
|
|
76
93
|
return;
|
|
77
94
|
}
|
|
78
95
|
if (sub === "preview") {
|
|
@@ -82,10 +99,14 @@ export async function handleReport(client, args, global) {
|
|
|
82
99
|
const startDate = String(flags.from || flags.start || flags.startDate || "");
|
|
83
100
|
const endDate = String(flags.to || flags.end || flags.endDate || "");
|
|
84
101
|
if (!filePath || !startDate || !endDate) {
|
|
85
|
-
throw new Error("usage: gamealgo report preview --pack report-pack.json --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--format json|csv] [--out file-or-directory]");
|
|
102
|
+
throw new Error("usage: gamealgo report preview --pack report-pack.json --from YYYY-MM-DD --to YYYY-MM-DD [--platform ios|android|rest] [--tab name] [--group name] [--chart name] [--selector k=v] [--sample-rows 0..20] [--full] [--format json|csv] [--out file-or-directory]");
|
|
86
103
|
}
|
|
104
|
+
requireReportQueryScope(flags, "report preview");
|
|
87
105
|
const content = await readJsonFile(filePath);
|
|
88
106
|
const timeoutMs = reportTimeoutMs(flags);
|
|
107
|
+
const format = reportOutputFormat(flags.format);
|
|
108
|
+
const full = reportFullOutput(flags) || format === "csv";
|
|
109
|
+
const sampleRows = reportSampleRows(flags, full);
|
|
89
110
|
const platform = await resolveCliReportPlatform(client, flags.platform, selectors.platform, startDate, endDate);
|
|
90
111
|
const startedAt = Date.now();
|
|
91
112
|
const stopProgress = startProgress("Previewing report results", timeoutMs);
|
|
@@ -103,19 +124,43 @@ export async function handleReport(client, args, global) {
|
|
|
103
124
|
chartId: optionalString(flags["chart-id"]),
|
|
104
125
|
platform,
|
|
105
126
|
selectors,
|
|
127
|
+
responseMode: full ? "full" : "summary",
|
|
128
|
+
sampleRows,
|
|
106
129
|
}, { timeoutMs });
|
|
107
130
|
}
|
|
108
131
|
finally {
|
|
109
132
|
stopProgress();
|
|
110
133
|
}
|
|
111
134
|
const elapsedMs = Date.now() - startedAt;
|
|
112
|
-
const
|
|
135
|
+
const rawOutputValue = withCliMeta(response, { elapsedMs, timeoutMs });
|
|
136
|
+
const statusSummary = summarizeReportDashboard(rawOutputValue, { command: "report.preview", sampleRows });
|
|
137
|
+
const outputValue = full ? rawOutputValue : statusSummary;
|
|
113
138
|
process.stderr.write(`Report preview finished in ${formatDuration(elapsedMs)}.\n`);
|
|
114
|
-
await writeReportQueryOutput(outputValue, flags, global, elapsedMs);
|
|
139
|
+
await writeReportQueryOutput(outputValue, flags, global, elapsedMs, statusSummary.ok);
|
|
115
140
|
return;
|
|
116
141
|
}
|
|
117
142
|
throw new Error("usage: gamealgo report <pull|validate|publish|manifest|result|preview>");
|
|
118
143
|
}
|
|
144
|
+
function reportFullOutput(flags) {
|
|
145
|
+
if (flags.full === undefined)
|
|
146
|
+
return false;
|
|
147
|
+
if (flags.full !== true)
|
|
148
|
+
throw new Error("--full does not accept a value");
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
function reportSampleRows(flags, full) {
|
|
152
|
+
const sampleRows = normalizeReportSampleRows(flags["sample-rows"]);
|
|
153
|
+
if (full && flags["sample-rows"] !== undefined) {
|
|
154
|
+
throw new Error("--sample-rows cannot be combined with --full or --format csv");
|
|
155
|
+
}
|
|
156
|
+
return sampleRows;
|
|
157
|
+
}
|
|
158
|
+
function requireReportQueryScope(flags, command) {
|
|
159
|
+
const scopeFlags = ["tab", "tab-id", "group", "group-id", "chart", "chart-id"];
|
|
160
|
+
if (scopeFlags.some((flag) => optionalString(flags[flag])))
|
|
161
|
+
return;
|
|
162
|
+
throw new Error(`${command} requires one of --tab, --tab-id, --group, --group-id, --chart, or --chart-id`);
|
|
163
|
+
}
|
|
119
164
|
export function reportOutputFormat(value) {
|
|
120
165
|
if (value === undefined || value === null || value === "")
|
|
121
166
|
return "json";
|
|
@@ -126,7 +171,7 @@ export function reportOutputFormat(value) {
|
|
|
126
171
|
return normalized;
|
|
127
172
|
throw new Error("--format must be json or csv");
|
|
128
173
|
}
|
|
129
|
-
async function writeReportQueryOutput(outputValue, flags, global, elapsedMs) {
|
|
174
|
+
async function writeReportQueryOutput(outputValue, flags, global, elapsedMs, succeeded) {
|
|
130
175
|
const format = reportOutputFormat(flags.format);
|
|
131
176
|
const out = optionalString(flags.out);
|
|
132
177
|
if (flags.out === true)
|
|
@@ -143,7 +188,7 @@ async function writeReportQueryOutput(outputValue, flags, global, elapsedMs) {
|
|
|
143
188
|
}
|
|
144
189
|
const first = files[0];
|
|
145
190
|
const summary = files.length === 1 ? {
|
|
146
|
-
ok:
|
|
191
|
+
ok: succeeded,
|
|
147
192
|
out: first.out,
|
|
148
193
|
format,
|
|
149
194
|
reportId: first.exported.reportId,
|
|
@@ -154,7 +199,7 @@ async function writeReportQueryOutput(outputValue, flags, global, elapsedMs) {
|
|
|
154
199
|
truncated: first.exported.truncated,
|
|
155
200
|
elapsedMs,
|
|
156
201
|
} : {
|
|
157
|
-
ok:
|
|
202
|
+
ok: succeeded,
|
|
158
203
|
out,
|
|
159
204
|
format,
|
|
160
205
|
results: files.length,
|
|
@@ -175,19 +220,28 @@ async function writeReportQueryOutput(outputValue, flags, global, elapsedMs) {
|
|
|
175
220
|
const exported = reportDashboardCsv(outputValue);
|
|
176
221
|
warnTruncatedCsv(exported);
|
|
177
222
|
process.stdout.write(exported.content);
|
|
223
|
+
if (!succeeded)
|
|
224
|
+
markCommandFailed();
|
|
178
225
|
return;
|
|
179
226
|
}
|
|
180
227
|
if (out) {
|
|
181
228
|
await writeTextFile(out, JSON.stringify(outputValue, null, 2) + "\n");
|
|
229
|
+
const outputObject = outputValue && typeof outputValue === "object" && !Array.isArray(outputValue)
|
|
230
|
+
? outputValue
|
|
231
|
+
: {};
|
|
182
232
|
await printResult({
|
|
183
|
-
ok:
|
|
233
|
+
ok: succeeded,
|
|
184
234
|
out,
|
|
185
|
-
results: Array.isArray(
|
|
235
|
+
results: Array.isArray(outputObject.results)
|
|
236
|
+
? outputObject.results.length
|
|
237
|
+
: Array.isArray(outputObject.items) ? outputObject.items.length : 0,
|
|
186
238
|
elapsedMs,
|
|
187
239
|
}, global);
|
|
188
240
|
return;
|
|
189
241
|
}
|
|
190
242
|
await printResult(outputValue, global);
|
|
243
|
+
if (!succeeded)
|
|
244
|
+
markCommandFailed();
|
|
191
245
|
}
|
|
192
246
|
function reportCsvOutputFiles(exports, out) {
|
|
193
247
|
if (exports.length === 1)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { objectValue } from "./resources.js";
|
|
2
|
+
export function diffReportPackEventCatalog(reportPackValue, catalogResponseValue) {
|
|
3
|
+
const reportPack = objectValue(reportPackValue);
|
|
4
|
+
if (!reportPack)
|
|
5
|
+
throw new Error("report pack must be a JSON object");
|
|
6
|
+
const reportEvents = reportPack.events === undefined ? {} : objectValue(reportPack.events);
|
|
7
|
+
if (!reportEvents)
|
|
8
|
+
throw new Error("report pack events must be an object");
|
|
9
|
+
const catalogResponse = objectValue(catalogResponseValue);
|
|
10
|
+
if (!catalogResponse || !Array.isArray(catalogResponse.events)) {
|
|
11
|
+
throw new Error("event catalog response is missing events");
|
|
12
|
+
}
|
|
13
|
+
const catalogEvents = catalogResponse.events.flatMap((rawEvent) => {
|
|
14
|
+
const event = objectValue(rawEvent);
|
|
15
|
+
const eventType = stringValue(event?.eventType);
|
|
16
|
+
return event && eventType ? [{ eventType, event }] : [];
|
|
17
|
+
});
|
|
18
|
+
const catalogByType = new Map(catalogEvents.map(({ eventType, event }) => [eventType, event]));
|
|
19
|
+
const errors = [];
|
|
20
|
+
const items = [];
|
|
21
|
+
let missingEventCount = 0;
|
|
22
|
+
let missingFieldCount = 0;
|
|
23
|
+
let typeConflictCount = 0;
|
|
24
|
+
let blockedEventCount = 0;
|
|
25
|
+
let deprecatedEventCount = 0;
|
|
26
|
+
for (const [eventType, rawReportEvent] of Object.entries(reportEvents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
27
|
+
const reportEvent = objectValue(rawReportEvent);
|
|
28
|
+
const reportFields = objectValue(reportEvent?.fields) ?? {};
|
|
29
|
+
const catalogEvent = catalogByType.get(eventType);
|
|
30
|
+
const missingFields = [];
|
|
31
|
+
const typeConflicts = [];
|
|
32
|
+
if (!catalogEvent) {
|
|
33
|
+
missingEventCount += 1;
|
|
34
|
+
for (const [fieldId, rawReportField] of Object.entries(reportFields).sort(([left], [right]) => left.localeCompare(right))) {
|
|
35
|
+
missingFields.push({ id: fieldId, type: stringValue(objectValue(rawReportField)?.type) });
|
|
36
|
+
}
|
|
37
|
+
missingFieldCount += missingFields.length;
|
|
38
|
+
errors.push({ scope: `event:${eventType}`, message: "Event is not registered in the event catalog" });
|
|
39
|
+
items.push({
|
|
40
|
+
eventType,
|
|
41
|
+
catalogStatus: "missing",
|
|
42
|
+
...(missingFields.length ? { missingFields } : {}),
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const catalogStatus = stringValue(catalogEvent.status) || "active";
|
|
47
|
+
if (catalogStatus === "blocked") {
|
|
48
|
+
blockedEventCount += 1;
|
|
49
|
+
errors.push({ scope: `event:${eventType}`, message: "Event is blocked in the event catalog" });
|
|
50
|
+
}
|
|
51
|
+
else if (catalogStatus === "deprecated") {
|
|
52
|
+
deprecatedEventCount += 1;
|
|
53
|
+
errors.push({ scope: `event:${eventType}`, message: "Event is deprecated in the event catalog" });
|
|
54
|
+
}
|
|
55
|
+
const catalogFields = objectValue(catalogEvent.fields) ?? {};
|
|
56
|
+
for (const [fieldId, rawReportField] of Object.entries(reportFields).sort(([left], [right]) => left.localeCompare(right))) {
|
|
57
|
+
const reportType = stringValue(objectValue(rawReportField)?.type);
|
|
58
|
+
const catalogField = objectValue(catalogFields[fieldId]);
|
|
59
|
+
if (!catalogField) {
|
|
60
|
+
missingFields.push({ id: fieldId, type: reportType });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const catalogType = stringValue(catalogField.type);
|
|
64
|
+
if (reportType && catalogType && reportType !== catalogType) {
|
|
65
|
+
typeConflicts.push({ id: fieldId, reportType, catalogType });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
missingFieldCount += missingFields.length;
|
|
69
|
+
typeConflictCount += typeConflicts.length;
|
|
70
|
+
if (missingFields.length) {
|
|
71
|
+
errors.push({
|
|
72
|
+
scope: `event:${eventType}`,
|
|
73
|
+
message: `${missingFields.length} report field(s) are not registered in the event catalog`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (typeConflicts.length) {
|
|
77
|
+
errors.push({
|
|
78
|
+
scope: `event:${eventType}`,
|
|
79
|
+
message: `${typeConflicts.length} report field type(s) conflict with the event catalog`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (catalogStatus !== "active" || missingFields.length || typeConflicts.length) {
|
|
83
|
+
items.push({
|
|
84
|
+
eventType,
|
|
85
|
+
catalogStatus,
|
|
86
|
+
...(missingFields.length ? { missingFields } : {}),
|
|
87
|
+
...(typeConflicts.length ? { typeConflicts } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
schemaVersion: 1,
|
|
93
|
+
command: "events.diff",
|
|
94
|
+
ok: errors.length === 0,
|
|
95
|
+
summary: {
|
|
96
|
+
reportEventCount: Object.keys(reportEvents).length,
|
|
97
|
+
catalogEventCount: catalogEvents.length,
|
|
98
|
+
issueEventCount: items.length,
|
|
99
|
+
missingEventCount,
|
|
100
|
+
missingFieldCount,
|
|
101
|
+
typeConflictCount,
|
|
102
|
+
blockedEventCount,
|
|
103
|
+
deprecatedEventCount,
|
|
104
|
+
},
|
|
105
|
+
errors,
|
|
106
|
+
warnings: [],
|
|
107
|
+
items,
|
|
108
|
+
truncated: false,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function stringValue(value) {
|
|
112
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
113
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { formatDuration } from "./output.js";
|
|
2
2
|
import { normalizeHost } from "./utils.js";
|
|
3
|
+
export class HttpRequestError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
errorCode;
|
|
6
|
+
details;
|
|
7
|
+
constructor(status, message, errorCode, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "HttpRequestError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.errorCode = errorCode;
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
3
15
|
export async function requestJson(host, path, init) {
|
|
4
16
|
const { timeoutMs, ...requestInit } = init;
|
|
5
17
|
let response;
|
|
@@ -18,13 +30,30 @@ export async function requestJson(host, path, init) {
|
|
|
18
30
|
const text = await response.text();
|
|
19
31
|
const json = parseJsonResponse(text);
|
|
20
32
|
if (!response.ok) {
|
|
21
|
-
|
|
33
|
+
const body = json && typeof json === "object" && !Array.isArray(json) ? json : undefined;
|
|
34
|
+
const errorCode = typeof body?.error === "string" ? body.error : undefined;
|
|
35
|
+
const details = body?.details && typeof body.details === "object" && !Array.isArray(body.details)
|
|
36
|
+
? body.details
|
|
37
|
+
: undefined;
|
|
38
|
+
throw new HttpRequestError(response.status, formatHttpError(response.status, response.statusText, text, json), errorCode, details);
|
|
22
39
|
}
|
|
23
40
|
if (text && json === undefined) {
|
|
24
41
|
throw new Error(`Invalid JSON response from ${path}: ${truncateBody(text)}`);
|
|
25
42
|
}
|
|
26
43
|
return json ?? null;
|
|
27
44
|
}
|
|
45
|
+
export function cliErrorPayload(error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (!(error instanceof HttpRequestError))
|
|
48
|
+
return { ok: false, error: message };
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: message,
|
|
52
|
+
status: error.status,
|
|
53
|
+
...(error.errorCode ? { errorCode: error.errorCode } : {}),
|
|
54
|
+
...(error.details ? { details: error.details } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
28
57
|
export function parseJsonResponse(text) {
|
|
29
58
|
if (!text)
|
|
30
59
|
return null;
|
package/dist/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { handleSkill } from "./commands/skill.js";
|
|
|
13
13
|
import { loadPackageMetadata } from "./config.js";
|
|
14
14
|
import { handleDocs, handleHelp } from "./docs.js";
|
|
15
15
|
import { handleDynamicConfig } from "./commands/dynamic-config.js";
|
|
16
|
+
import { cliErrorPayload } from "./http.js";
|
|
16
17
|
import { printResult } from "./output.js";
|
|
17
18
|
export async function main() {
|
|
18
19
|
const args = process.argv.slice(2);
|
|
@@ -110,7 +111,7 @@ export async function main() {
|
|
|
110
111
|
catch (error) {
|
|
111
112
|
const message = error instanceof Error ? error.message : String(error);
|
|
112
113
|
if (global.json) {
|
|
113
|
-
console.log(JSON.stringify(
|
|
114
|
+
console.log(JSON.stringify(cliErrorPayload(error), null, 2));
|
|
114
115
|
}
|
|
115
116
|
else {
|
|
116
117
|
console.error(`Error: ${message}`);
|
package/dist/output.js
CHANGED
|
@@ -62,6 +62,7 @@ export function formatDuration(ms) {
|
|
|
62
62
|
export async function printResult(value, global) {
|
|
63
63
|
if (global.json) {
|
|
64
64
|
console.log(JSON.stringify(value, null, 2));
|
|
65
|
+
markExplicitFailure(value);
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
if (typeof value === "string") {
|
|
@@ -69,6 +70,15 @@ export async function printResult(value, global) {
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
console.log(YAML.stringify(value).trimEnd());
|
|
73
|
+
markExplicitFailure(value);
|
|
74
|
+
}
|
|
75
|
+
export function markCommandFailed() {
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
function markExplicitFailure(value) {
|
|
79
|
+
if (value && typeof value === "object" && !Array.isArray(value) && value.ok === false) {
|
|
80
|
+
markCommandFailed();
|
|
81
|
+
}
|
|
72
82
|
}
|
|
73
83
|
export async function confirm(question) {
|
|
74
84
|
const rl = createInterface({ input, output });
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
export const REPORT_SUMMARY_SCHEMA_VERSION = 1;
|
|
2
|
+
export const REPORT_SUMMARY_MAX_ITEMS = 200;
|
|
3
|
+
export const REPORT_SUMMARY_MAX_ISSUES = 100;
|
|
4
|
+
export const REPORT_SUMMARY_MAX_SAMPLE_ROWS = 20;
|
|
5
|
+
export function summarizeReportValidation(value) {
|
|
6
|
+
const root = objectValue(value);
|
|
7
|
+
const validation = objectValue(root?.validation);
|
|
8
|
+
const errorMessages = stringValues(validation?.errors);
|
|
9
|
+
const warningMessages = stringValues(validation?.warnings);
|
|
10
|
+
const statements = arrayValues(objectValue(root?.sqlPreview)?.statements);
|
|
11
|
+
const statementItems = statements.map((raw) => {
|
|
12
|
+
const statement = objectValue(raw);
|
|
13
|
+
const sql = typeof statement?.sql === "string" ? statement.sql : "";
|
|
14
|
+
const declaredBytes = finiteNonNegativeNumber(statement?.bytes);
|
|
15
|
+
return compactObject({
|
|
16
|
+
kind: "sql",
|
|
17
|
+
id: stringValue(statement?.id),
|
|
18
|
+
title: stringValue(statement?.title),
|
|
19
|
+
bytes: declaredBytes ?? utf8Bytes(sql),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
const errors = issueValues(errorMessages, "validation");
|
|
23
|
+
const warnings = issueValues(warningMessages, "validation");
|
|
24
|
+
const items = statementItems.slice(0, REPORT_SUMMARY_MAX_ITEMS);
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: REPORT_SUMMARY_SCHEMA_VERSION,
|
|
27
|
+
command: "report.validate",
|
|
28
|
+
ok: validation?.ok === true && errorMessages.length === 0,
|
|
29
|
+
summary: {
|
|
30
|
+
errorCount: errorMessages.length,
|
|
31
|
+
warningCount: warningMessages.length,
|
|
32
|
+
eventTypeCount: arrayValues(validation?.eventTypes).length,
|
|
33
|
+
datasetCount: arrayValues(validation?.datasets).length,
|
|
34
|
+
reportCount: arrayValues(validation?.reports).length,
|
|
35
|
+
sqlStatementCount: statements.length,
|
|
36
|
+
},
|
|
37
|
+
errors: errors.values,
|
|
38
|
+
warnings: warnings.values,
|
|
39
|
+
items,
|
|
40
|
+
truncated: errors.truncated || warnings.truncated || statementItems.length > items.length,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function summarizeReportManifest(value) {
|
|
44
|
+
const root = objectValue(value);
|
|
45
|
+
const reportPack = compactReportPack(root?.reportPack);
|
|
46
|
+
const manifest = objectValue(root?.manifest);
|
|
47
|
+
const tabs = arrayValues(manifest?.tabs);
|
|
48
|
+
const experimentViews = arrayValues(manifest?.experimentViews);
|
|
49
|
+
const reportIds = arrayValues(manifest?.reports)
|
|
50
|
+
.map((report) => typeof report === "string" ? report : stringValue(objectValue(report)?.id))
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const returnedReportIds = [...new Set(reportIds)].slice(0, REPORT_SUMMARY_MAX_ITEMS);
|
|
53
|
+
const allItems = [];
|
|
54
|
+
let groupCount = 0;
|
|
55
|
+
let chartCount = 0;
|
|
56
|
+
for (const rawTab of tabs) {
|
|
57
|
+
const tab = objectValue(rawTab);
|
|
58
|
+
const groups = arrayValues(tab?.groups);
|
|
59
|
+
groupCount += groups.length;
|
|
60
|
+
for (const rawGroup of groups) {
|
|
61
|
+
const group = objectValue(rawGroup);
|
|
62
|
+
const charts = arrayValues(group?.charts);
|
|
63
|
+
chartCount += charts.length;
|
|
64
|
+
for (const rawChart of charts) {
|
|
65
|
+
allItems.push(chartManifestItem(rawChart, group, {
|
|
66
|
+
scope: "dashboard",
|
|
67
|
+
tabId: stringValue(tab?.id),
|
|
68
|
+
tabTitle: stringValue(tab?.title),
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const rawView of experimentViews) {
|
|
74
|
+
const view = objectValue(rawView);
|
|
75
|
+
const groups = arrayValues(view?.groups);
|
|
76
|
+
groupCount += groups.length;
|
|
77
|
+
for (const rawGroup of groups) {
|
|
78
|
+
const group = objectValue(rawGroup);
|
|
79
|
+
const charts = arrayValues(group?.charts);
|
|
80
|
+
chartCount += charts.length;
|
|
81
|
+
for (const rawChart of charts) {
|
|
82
|
+
allItems.push(chartManifestItem(rawChart, group, {
|
|
83
|
+
scope: "experiment",
|
|
84
|
+
strategy: stringValue(view?.strategy),
|
|
85
|
+
viewTitle: stringValue(view?.title),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const items = allItems.slice(0, REPORT_SUMMARY_MAX_ITEMS);
|
|
91
|
+
return {
|
|
92
|
+
schemaVersion: REPORT_SUMMARY_SCHEMA_VERSION,
|
|
93
|
+
command: "report.manifest",
|
|
94
|
+
ok: Boolean(manifest),
|
|
95
|
+
summary: compactObject({
|
|
96
|
+
reportPack,
|
|
97
|
+
title: stringValue(manifest?.title),
|
|
98
|
+
tabCount: tabs.length,
|
|
99
|
+
experimentViewCount: experimentViews.length,
|
|
100
|
+
groupCount,
|
|
101
|
+
chartCount,
|
|
102
|
+
reportCount: reportIds.length,
|
|
103
|
+
reportIds: returnedReportIds,
|
|
104
|
+
returnedItemCount: items.length,
|
|
105
|
+
}),
|
|
106
|
+
errors: manifest ? [] : [{ scope: "manifest", message: "Report manifest response is missing manifest" }],
|
|
107
|
+
warnings: [],
|
|
108
|
+
items,
|
|
109
|
+
truncated: allItems.length > items.length || reportIds.length > returnedReportIds.length,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function summarizeReportDashboard(value, options) {
|
|
113
|
+
const root = objectValue(value);
|
|
114
|
+
const results = arrayValues(root?.results);
|
|
115
|
+
const requestedSampleRows = normalizeReportSampleRows(options.sampleRows);
|
|
116
|
+
const allErrors = [];
|
|
117
|
+
const allWarnings = [];
|
|
118
|
+
const allItems = [];
|
|
119
|
+
const statusCounts = {};
|
|
120
|
+
let totalRows = 0;
|
|
121
|
+
let sourceTruncated = false;
|
|
122
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
123
|
+
const result = objectValue(results[index]);
|
|
124
|
+
const chart = objectValue(result?.chart);
|
|
125
|
+
const tab = objectValue(result?.tab);
|
|
126
|
+
const group = objectValue(result?.group);
|
|
127
|
+
const chartId = stringValue(chart?.id) || `result-${index + 1}`;
|
|
128
|
+
const scope = `chart:${chartId}`;
|
|
129
|
+
const rows = arrayValues(result?.rows);
|
|
130
|
+
const rowCount = finiteNonNegativeNumber(result?.rowCount) ?? rows.length;
|
|
131
|
+
const status = reportResultStatus(result);
|
|
132
|
+
const error = stringValue(result?.error);
|
|
133
|
+
const truncated = result?.truncated === true;
|
|
134
|
+
totalRows += rowCount;
|
|
135
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
136
|
+
if (error)
|
|
137
|
+
allErrors.push({ scope, message: error });
|
|
138
|
+
if (truncated) {
|
|
139
|
+
sourceTruncated = true;
|
|
140
|
+
const rowLimit = finiteNonNegativeNumber(result?.rowLimit);
|
|
141
|
+
allWarnings.push({
|
|
142
|
+
scope,
|
|
143
|
+
message: `Report result is truncated${rowLimit === undefined ? "" : ` at ${rowLimit} rows`}`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (result?.stale === true) {
|
|
147
|
+
allWarnings.push({ scope, message: "Report result is stale" });
|
|
148
|
+
}
|
|
149
|
+
allItems.push(compactObject({
|
|
150
|
+
kind: "chart-result",
|
|
151
|
+
status,
|
|
152
|
+
tabId: stringValue(tab?.id),
|
|
153
|
+
tabTitle: stringValue(tab?.title),
|
|
154
|
+
groupId: stringValue(group?.id),
|
|
155
|
+
groupTitle: stringValue(group?.title),
|
|
156
|
+
chartId,
|
|
157
|
+
chartTitle: stringValue(chart?.title),
|
|
158
|
+
chartType: stringValue(chart?.type),
|
|
159
|
+
reportId: stringValue(result?.reportId),
|
|
160
|
+
columns: columnNames(result?.columns),
|
|
161
|
+
rowCount,
|
|
162
|
+
cached: typeof result?.cached === "boolean" ? result.cached : undefined,
|
|
163
|
+
cachedAt: stringValue(result?.cachedAt),
|
|
164
|
+
stale: typeof result?.stale === "boolean" ? result.stale : undefined,
|
|
165
|
+
truncated,
|
|
166
|
+
rowLimit: finiteNonNegativeNumber(result?.rowLimit),
|
|
167
|
+
error,
|
|
168
|
+
sampleRows: requestedSampleRows > 0 ? rows.slice(0, requestedSampleRows) : undefined,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
const errors = limitIssues(allErrors);
|
|
172
|
+
const warnings = limitIssues(allWarnings);
|
|
173
|
+
const items = allItems.slice(0, REPORT_SUMMARY_MAX_ITEMS);
|
|
174
|
+
const incompleteCount = Object.entries(statusCounts)
|
|
175
|
+
.filter(([status]) => status !== "ready")
|
|
176
|
+
.reduce((sum, [, count]) => sum + count, 0);
|
|
177
|
+
return {
|
|
178
|
+
schemaVersion: REPORT_SUMMARY_SCHEMA_VERSION,
|
|
179
|
+
command: options.command,
|
|
180
|
+
ok: allErrors.length === 0 && incompleteCount === 0,
|
|
181
|
+
summary: compactObject({
|
|
182
|
+
reportPack: compactReportPack(root?.reportPack),
|
|
183
|
+
dateRange: objectValue(root?.dateRange),
|
|
184
|
+
selectors: objectValue(root?.selectors),
|
|
185
|
+
resultCount: results.length,
|
|
186
|
+
statusCounts,
|
|
187
|
+
totalRows,
|
|
188
|
+
sampleRowsPerResult: requestedSampleRows,
|
|
189
|
+
returnedItemCount: items.length,
|
|
190
|
+
cli: objectValue(root?.cli),
|
|
191
|
+
}),
|
|
192
|
+
errors: errors.values,
|
|
193
|
+
warnings: warnings.values,
|
|
194
|
+
items,
|
|
195
|
+
truncated: sourceTruncated || errors.truncated || warnings.truncated || allItems.length > items.length,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function normalizeReportSampleRows(value) {
|
|
199
|
+
if (value === undefined || value === null || value === "")
|
|
200
|
+
return 0;
|
|
201
|
+
if (typeof value === "boolean")
|
|
202
|
+
throw new Error("--sample-rows requires a non-negative integer");
|
|
203
|
+
const parsed = Number(value);
|
|
204
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > REPORT_SUMMARY_MAX_SAMPLE_ROWS) {
|
|
205
|
+
throw new Error(`--sample-rows must be an integer between 0 and ${REPORT_SUMMARY_MAX_SAMPLE_ROWS}`);
|
|
206
|
+
}
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
function chartManifestItem(rawChart, group, location) {
|
|
210
|
+
const chart = objectValue(rawChart);
|
|
211
|
+
const views = objectValue(chart?.views);
|
|
212
|
+
const reportIds = new Set();
|
|
213
|
+
const reportId = stringValue(chart?.report);
|
|
214
|
+
if (reportId)
|
|
215
|
+
reportIds.add(reportId);
|
|
216
|
+
for (const rawView of Object.values(views ?? {})) {
|
|
217
|
+
const viewReportId = stringValue(objectValue(rawView)?.report);
|
|
218
|
+
if (viewReportId)
|
|
219
|
+
reportIds.add(viewReportId);
|
|
220
|
+
}
|
|
221
|
+
return compactObject({
|
|
222
|
+
kind: "chart",
|
|
223
|
+
...location,
|
|
224
|
+
groupId: stringValue(group?.id),
|
|
225
|
+
groupTitle: stringValue(group?.title),
|
|
226
|
+
standardRef: stringValue(group?.standardRef),
|
|
227
|
+
chartId: stringValue(chart?.id),
|
|
228
|
+
chartTitle: stringValue(chart?.title),
|
|
229
|
+
chartType: stringValue(chart?.type),
|
|
230
|
+
reportIds: [...reportIds],
|
|
231
|
+
selectorIds: arrayValues(group?.selectors)
|
|
232
|
+
.map((selector) => stringValue(objectValue(selector)?.id))
|
|
233
|
+
.filter(Boolean),
|
|
234
|
+
viewIds: views ? Object.keys(views) : [],
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function reportResultStatus(result) {
|
|
238
|
+
if (stringValue(result?.error))
|
|
239
|
+
return "error";
|
|
240
|
+
if (result?.pending === true)
|
|
241
|
+
return "pending";
|
|
242
|
+
if (result?.retrying === true)
|
|
243
|
+
return "retrying";
|
|
244
|
+
if (result?.refreshing === true)
|
|
245
|
+
return "refreshing";
|
|
246
|
+
if (result?.stale === true)
|
|
247
|
+
return "stale";
|
|
248
|
+
return "ready";
|
|
249
|
+
}
|
|
250
|
+
function compactReportPack(value) {
|
|
251
|
+
const reportPack = objectValue(value);
|
|
252
|
+
if (!reportPack)
|
|
253
|
+
return undefined;
|
|
254
|
+
return compactObject({
|
|
255
|
+
gameId: stringValue(reportPack.gameId),
|
|
256
|
+
version: stringValue(reportPack.version),
|
|
257
|
+
status: stringValue(reportPack.status),
|
|
258
|
+
updatedAt: stringValue(reportPack.updatedAt),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function columnNames(value) {
|
|
262
|
+
return arrayValues(value).flatMap((column) => {
|
|
263
|
+
if (typeof column === "string")
|
|
264
|
+
return column ? [column] : [];
|
|
265
|
+
const object = objectValue(column);
|
|
266
|
+
const name = stringValue(object?.name) || stringValue(object?.id);
|
|
267
|
+
return name ? [name] : [];
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function issueValues(messages, scope) {
|
|
271
|
+
return limitIssues(messages.map((message) => ({ scope, message })));
|
|
272
|
+
}
|
|
273
|
+
function limitIssues(values) {
|
|
274
|
+
return {
|
|
275
|
+
values: values.slice(0, REPORT_SUMMARY_MAX_ISSUES),
|
|
276
|
+
truncated: values.length > REPORT_SUMMARY_MAX_ISSUES,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function objectValue(value) {
|
|
280
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
281
|
+
}
|
|
282
|
+
function arrayValues(value) {
|
|
283
|
+
return Array.isArray(value) ? value : [];
|
|
284
|
+
}
|
|
285
|
+
function stringValues(value) {
|
|
286
|
+
return arrayValues(value).filter((item) => typeof item === "string");
|
|
287
|
+
}
|
|
288
|
+
function stringValue(value) {
|
|
289
|
+
return typeof value === "string" && value ? value : undefined;
|
|
290
|
+
}
|
|
291
|
+
function finiteNonNegativeNumber(value) {
|
|
292
|
+
const number = Number(value);
|
|
293
|
+
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
294
|
+
}
|
|
295
|
+
function compactObject(value) {
|
|
296
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
297
|
+
}
|
|
298
|
+
function utf8Bytes(value) {
|
|
299
|
+
return new TextEncoder().encode(value).byteLength;
|
|
300
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -14,6 +14,12 @@ export function integrationPlatform(value) {
|
|
|
14
14
|
return platform;
|
|
15
15
|
throw new Error("--platform must be ios, android, maker, or rest");
|
|
16
16
|
}
|
|
17
|
+
export function experimentPlatform(value) {
|
|
18
|
+
const platform = optionalString(value);
|
|
19
|
+
if (platform === "ios" || platform === "android" || platform === "rest")
|
|
20
|
+
return platform;
|
|
21
|
+
throw new Error("--platform must be ios, android, or rest for experiments");
|
|
22
|
+
}
|
|
17
23
|
export function optionalString(value) {
|
|
18
24
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
19
25
|
}
|
package/package.json
CHANGED
package/skills/gamealgo/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gamealgo
|
|
3
|
-
description: Integrate and improve games with GameAlgo using the project-bound CLI, live Server documentation, and the Server-maintained optimization task catalog. Use when working on GameAlgo SDK setup, telemetry events, report packs, Adjust attribution, remote strategies, experiments,
|
|
3
|
+
description: Integrate and improve games with GameAlgo using the project-bound CLI, live Server documentation, and the Server-maintained optimization task catalog. Use when working on GameAlgo SDK setup, telemetry events, report packs, Adjust attribution, remote strategies, experiments, discovering supported GameAlgo iteration tasks, or changing player-facing game behavior that may need telemetry coverage.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# GameAlgo
|
|
@@ -42,6 +42,12 @@ Use each topic's section list as the router. Read the whole topic only when the
|
|
|
42
42
|
|
|
43
43
|
If work crosses domains, combine their requirements. For example, an advertising event used by an experiment report requires the integration plan, event inventory, SDK API documentation, relevant report sections, and relevant experiment sections. Read a section once per turn unless the task expands or the Server documentation version changes.
|
|
44
44
|
|
|
45
|
+
For `report validate`, `report manifest`, `report preview`, and JSON `report result`, use the default compact output. It provides the stable `schemaVersion`, `command`, `ok`, `summary`, `errors`, `warnings`, `items`, and `truncated` fields needed by an agent; do not recreate summaries with ad-hoc `jq`. Add `--sample-rows <n>` only when a small value sample is necessary. Use `--full` only when the task genuinely requires complete generated SQL, chart definitions, or every returned row; use CSV export rather than full JSON when the goal is data extraction.
|
|
46
|
+
|
|
47
|
+
When changing an existing Report Pack, run `gamealgo report pull --out <working-pack.json>` before editing and use that current published Pack as the baseline. Do not start from a remembered or stale local copy. Run `gamealgo events diff --pack <working-pack.json>` after defining the Pack events; if it reports missing fields, type conflicts, or blocked/deprecated events, inspect the actual game call sites and prepare real business descriptions rather than inventing catalog text from field names. The diff is read-only and does not authorize `events apply` or event activation.
|
|
48
|
+
|
|
49
|
+
Validate with the compact default output, then preview every chart or group affected by the change without querying unrelated tabs. A report query must explicitly select a tab, group, or chart. A globally unique bare chart id is accepted; if the Server returns `ambiguous_chart_id`, use its candidate ids and the local Pack structure to retry without asking the developer to choose an internal chart id. Before applying event metadata or publishing the Pack, show the exact changes and impact unless the developer already authorized them. After publishing, pull the effective Pack again and verify the expected manifest or report result.
|
|
50
|
+
|
|
45
51
|
For initial integration or business dashboard work, standard dashboards are only the baseline. During the initial integration, inspect the actual game loop and define a separate game-specific new-user flow dashboard with meaningful milestones and drop-off diagnostics. Add required milestone telemetry and event catalog fields in the same integration instead of waiting for production volume. Validate the pipeline with test data; label business conclusions as awaiting data when production samples are immature, but do not omit the dashboard design.
|
|
46
52
|
|
|
47
53
|
## Follow Mandatory Checkpoints
|
|
@@ -81,6 +87,18 @@ Reuse events with the same business meaning, register new fields with their real
|
|
|
81
87
|
|
|
82
88
|
For every game, prefer stable, human-readable Chinese values for business dimensions that will appear directly in report legends, axes, selectors, or tables. This includes values such as level or stage type, game mode, placement, item name, and business outcome or failure reason. Keep event names, field names, IDs, SQL aliases, and protocol-defined enums in their required technical format. Do not rename values already used by a live game solely to translate them, because that would split one historical dimension into multiple report values.
|
|
83
89
|
|
|
90
|
+
## Revisit Telemetry When Gameplay Changes
|
|
91
|
+
|
|
92
|
+
When work adds or materially changes player-facing gameplay, onboarding, progression, economy, advertising, purchases, rewards, or important success and failure paths, proactively compare the new behavior with the existing GameAlgo telemetry. Derive the affected behavior from the current conversation and repository diff; do not make the developer restate the feature in a separate event configuration or prompt.
|
|
93
|
+
|
|
94
|
+
- Do not add telemetry mechanically for visual-only changes, refactors, or behavior already covered by an event with the same semantics.
|
|
95
|
+
- If existing telemetry is sufficient, say so briefly and do not ask a redundant question.
|
|
96
|
+
- If a meaningful decision or funnel step would remain unobservable, present a concise proposal as soon as the gap is clear: the question it answers, the standard or existing event to reuse (or the new custom event), exact trigger boundary, essential fields, expected volume bound, and any report it enables. Ask whether to include that telemetry in the current change; do not silently expand a feature request into new data collection.
|
|
97
|
+
- If the developer explicitly asks to update or optimize telemetry from the current context, treat that as authorization to inspect all relevant changes in the conversation and implement one coherent local telemetry patch without asking them to enumerate the features again. It does not by itself authorize publishing event-catalog or Report Pack changes; disclose those exact external mutations and follow the normal publication checkpoint.
|
|
98
|
+
- When telemetry was already explicitly requested, implement it with the feature instead of asking again.
|
|
99
|
+
|
|
100
|
+
After approval, update the actual emission call sites and their normal-flow tests together. Include catalog fields or reporting changes only when they are needed for the stated question, and then follow the event-volume and publication safeguards below.
|
|
101
|
+
|
|
84
102
|
## Bound Custom Event Volume
|
|
85
103
|
|
|
86
104
|
Before adding or changing a custom event, prefer a protocol-standard event or an existing custom event with the same meaning. Keep the event type stable and put variable values in payload fields; never generate event types from item IDs, levels, timestamps, user input, or other runtime values.
|
|
@@ -145,6 +163,15 @@ When client code implements an experiment or remote Strategy, define a stable, t
|
|
|
145
163
|
|
|
146
164
|
Before delivery, inspect changed client code for Variant or assignment-identity access and verify none of it reaches business conditionals. Test active control and treatment configs, missing configuration, and the post-promotion shape where the Variant becomes `default` while the promoted config remains active; unchanged parameters must produce unchanged game behavior regardless of the Variant name.
|
|
147
165
|
|
|
166
|
+
## Preserve Experiment Platform Scope
|
|
167
|
+
|
|
168
|
+
A Strategy key is shared by the game and keeps a global fallback default, while every Run belongs to exactly one runtime platform: `ios`, `android`, or `rest` (`rest` includes TapTap Maker). A promoted Run updates only its own platform default. Other platform defaults remain unchanged, and a platform without an explicit default continues to use the global fallback.
|
|
169
|
+
|
|
170
|
+
- When a Run includes `variantId: default`, the Server snapshots the effective default for that Run's platform, falling back to the global Strategy default only when that platform has no promoted default.
|
|
171
|
+
- `experiment run promote` needs no platform flag because the Run records its platform.
|
|
172
|
+
- When rolling a platform back to a global historical version, pass `experiment strategy rollback ... --platform <ios|android|rest>` so the global version is copied into that platform default rather than replacing the global fallback.
|
|
173
|
+
- Treat `experiment strategy publish` and `experiment strategy default` as global-fallback operations; they do not overwrite existing promoted platform defaults.
|
|
174
|
+
|
|
148
175
|
## Require Explicit Approval Before Publishing Experiments
|
|
149
176
|
|
|
150
177
|
Treat publishing a live experiment as a two-step workflow. Before approval, limit work to read-only analysis, experiment ideas, and local drafts. Do not publish an experiment-specific Strategy or Strategy version, create or start a Run, allocate traffic, or otherwise mutate live experiment state.
|