gamealgo-cli 0.4.1 → 0.4.3
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/README.md +19 -1
- package/dist/commands/report.js +140 -0
- package/package.json +1 -1
- package/skills/gamealgo/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -47,7 +47,25 @@ gamealgo report advanced run --query-id battle_analysis --from 2026-09-04 --to 2
|
|
|
47
47
|
gamealgo report advanced list
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
All Report Packs use `schemaVersion: 2`, with custom `groups[].views` or platform-owned `groups[].standard`. Advanced query runs are explicit, are never preloaded, and remain available as immutable reports for 30 days.
|
|
51
|
+
|
|
52
|
+
Managed marketing lifecycle queries can choose their runtime cohort shape without editing or publishing the Pack. For example, this returns one row per channel, Campaign, and country with D0–D30 retention as columns:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gamealgo report advanced run \
|
|
56
|
+
--query-id retention_drilldown \
|
|
57
|
+
--platform ios \
|
|
58
|
+
--from 2026-08-08 \
|
|
59
|
+
--to 2026-09-22 \
|
|
60
|
+
--observation-end 2026-09-22 \
|
|
61
|
+
--group-by network,campaign,country_code \
|
|
62
|
+
--day-offsets 0-30 \
|
|
63
|
+
--pivot-metric retention_rate \
|
|
64
|
+
--filter network=Meta \
|
|
65
|
+
--wait --out retention.json --json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Repeat `--filter field=value1,value2` for `cohort_dt`, `day_offset`, `network`, `campaign`, `campaign_id`, `country_code`, or `platform`. The CLI combines different fields with AND, merges repeated values for the same field, and automatically adds the internal `day_offset` grouping when `--group-by` is present. `--pivot-metric` accepts `retention_rate`, `active_users`, `daily_revenue`, `cumulative_revenue`, `ltv`, or `roas`; `--min-cohort-size` removes undersized groups.
|
|
51
69
|
|
|
52
70
|
Help and documentation are fetched from the configured Server on every invocation. The CLI intentionally does not keep a documentation cache.
|
|
53
71
|
|
package/dist/commands/report.js
CHANGED
|
@@ -165,6 +165,7 @@ export async function handleReport(client, args, global) {
|
|
|
165
165
|
async function handleAdvancedReport(client, args, global) {
|
|
166
166
|
const action = args.shift();
|
|
167
167
|
if (action === "run") {
|
|
168
|
+
const marketingFilters = parseMarketingCohortFilterFlags(args);
|
|
168
169
|
const flags = parseFlags(args);
|
|
169
170
|
const queryId = requiredTextFlag(flags["query-id"], "--query-id");
|
|
170
171
|
const startDate = requiredTextFlag(flags.from || flags.start || flags.startDate, "--from");
|
|
@@ -180,6 +181,7 @@ async function handleAdvancedReport(client, args, global) {
|
|
|
180
181
|
const pack = packPath ? await readJsonFile(packPath) : undefined;
|
|
181
182
|
if (pack !== undefined)
|
|
182
183
|
assertWritableReportPackV2(pack, "report advanced run");
|
|
184
|
+
const marketingCohort = marketingCohortRunOptions(flags, marketingFilters);
|
|
183
185
|
const wait = booleanFlag(flags.wait, "--wait");
|
|
184
186
|
const timeoutMs = reportTimeoutMs(flags) ?? 15 * 60 * 1000;
|
|
185
187
|
await announceTargetGame(client);
|
|
@@ -189,6 +191,7 @@ async function handleAdvancedReport(client, args, global) {
|
|
|
189
191
|
endDate,
|
|
190
192
|
platform,
|
|
191
193
|
...(pack !== undefined ? { pack } : {}),
|
|
194
|
+
...(marketingCohort ? { marketingCohort } : {}),
|
|
192
195
|
}, { timeoutMs: Math.min(timeoutMs, 30_000) });
|
|
193
196
|
if (!wait) {
|
|
194
197
|
await writeAdvancedOutput(created, flags, global);
|
|
@@ -229,6 +232,143 @@ async function handleAdvancedReport(client, args, global) {
|
|
|
229
232
|
}
|
|
230
233
|
throw new Error("usage: gamealgo report advanced <run|get|list>");
|
|
231
234
|
}
|
|
235
|
+
const MARKETING_COHORT_DIMENSIONS = new Set([
|
|
236
|
+
"cohort_dt",
|
|
237
|
+
"day_offset",
|
|
238
|
+
"network",
|
|
239
|
+
"campaign",
|
|
240
|
+
"campaign_id",
|
|
241
|
+
"country_code",
|
|
242
|
+
"platform",
|
|
243
|
+
]);
|
|
244
|
+
const MARKETING_COHORT_PIVOT_METRICS = new Set([
|
|
245
|
+
"retention_rate",
|
|
246
|
+
"active_users",
|
|
247
|
+
"daily_revenue",
|
|
248
|
+
"cumulative_revenue",
|
|
249
|
+
"ltv",
|
|
250
|
+
"roas",
|
|
251
|
+
]);
|
|
252
|
+
function parseMarketingCohortFilterFlags(args) {
|
|
253
|
+
const grouped = new Map();
|
|
254
|
+
for (let index = 0; index < args.length;) {
|
|
255
|
+
if (args[index] !== "--filter") {
|
|
256
|
+
index += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const expression = args[index + 1];
|
|
260
|
+
if (!expression || expression.startsWith("--"))
|
|
261
|
+
throw new Error("--filter requires field=value");
|
|
262
|
+
const separator = expression.indexOf("=");
|
|
263
|
+
if (separator <= 0)
|
|
264
|
+
throw new Error("--filter must use field=value or field=value1,value2");
|
|
265
|
+
const field = normalizeMarketingCohortDimension(expression.slice(0, separator).trim(), "--filter");
|
|
266
|
+
const values = expression.slice(separator + 1).split(",").map((value) => value.trim()).filter(Boolean);
|
|
267
|
+
if (!values.length)
|
|
268
|
+
throw new Error(`--filter ${field} requires at least one value`);
|
|
269
|
+
grouped.set(field, [...new Set([...(grouped.get(field) ?? []), ...values])]);
|
|
270
|
+
args.splice(index, 2);
|
|
271
|
+
}
|
|
272
|
+
return [...grouped].map(([field, values]) => ({ field, values }));
|
|
273
|
+
}
|
|
274
|
+
function marketingCohortRunOptions(flags, filters) {
|
|
275
|
+
const cohortFlagNames = ["observation-end", "group-by", "day-offsets", "min-cohort-size", "pivot-metric"];
|
|
276
|
+
if (!filters.length && !cohortFlagNames.some((flag) => flags[flag] !== undefined))
|
|
277
|
+
return undefined;
|
|
278
|
+
const observationEndDate = optionalTextFlag(flags["observation-end"], "--observation-end");
|
|
279
|
+
if (observationEndDate && !isIsoDate(observationEndDate)) {
|
|
280
|
+
throw new Error("--observation-end must use YYYY-MM-DD");
|
|
281
|
+
}
|
|
282
|
+
const groupByValue = optionalTextFlag(flags["group-by"], "--group-by");
|
|
283
|
+
const groupBy = groupByValue
|
|
284
|
+
? [...new Set(groupByValue.split(",")
|
|
285
|
+
.map((field) => normalizeMarketingCohortDimension(field.trim(), "--group-by"))
|
|
286
|
+
.filter(Boolean))]
|
|
287
|
+
: undefined;
|
|
288
|
+
if (groupBy && !groupBy.includes("day_offset"))
|
|
289
|
+
groupBy.push("day_offset");
|
|
290
|
+
const dayOffsetsValue = optionalTextFlag(flags["day-offsets"], "--day-offsets");
|
|
291
|
+
const dayOffsets = dayOffsetsValue ? parseMarketingCohortDayOffsets(dayOffsetsValue) : undefined;
|
|
292
|
+
const minCohortSizeValue = optionalTextFlag(flags["min-cohort-size"], "--min-cohort-size");
|
|
293
|
+
const minCohortSize = minCohortSizeValue === undefined ? undefined : Number(minCohortSizeValue);
|
|
294
|
+
if (minCohortSize !== undefined && (!Number.isInteger(minCohortSize) || minCohortSize < 1 || minCohortSize > 1_000_000)) {
|
|
295
|
+
throw new Error("--min-cohort-size must be an integer between 1 and 1000000");
|
|
296
|
+
}
|
|
297
|
+
const pivotMetric = optionalTextFlag(flags["pivot-metric"], "--pivot-metric");
|
|
298
|
+
if (pivotMetric && !MARKETING_COHORT_PIVOT_METRICS.has(pivotMetric)) {
|
|
299
|
+
throw new Error(`--pivot-metric must be one of ${[...MARKETING_COHORT_PIVOT_METRICS].join(", ")}`);
|
|
300
|
+
}
|
|
301
|
+
if (pivotMetric && !groupBy) {
|
|
302
|
+
throw new Error("--group-by is required when --pivot-metric is used");
|
|
303
|
+
}
|
|
304
|
+
const filter = filters.length === 0
|
|
305
|
+
? undefined
|
|
306
|
+
: filters.length === 1
|
|
307
|
+
? marketingCohortFilter(filters[0])
|
|
308
|
+
: { op: "and", filters: filters.map(marketingCohortFilter) };
|
|
309
|
+
return {
|
|
310
|
+
...(observationEndDate ? { observationEndDate } : {}),
|
|
311
|
+
...(groupBy ? { groupBy } : {}),
|
|
312
|
+
...(dayOffsets ? { dayOffsets } : {}),
|
|
313
|
+
...(minCohortSize !== undefined ? { minCohortSize } : {}),
|
|
314
|
+
...(pivotMetric ? { pivotMetric } : {}),
|
|
315
|
+
...(filter ? { filter } : {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function marketingCohortFilter(input) {
|
|
319
|
+
const values = input.field === "day_offset"
|
|
320
|
+
? input.values.map((value) => {
|
|
321
|
+
const day = Number(value);
|
|
322
|
+
if (!Number.isInteger(day) || day < 0 || day > 30) {
|
|
323
|
+
throw new Error("--filter day_offset values must be integers between 0 and 30");
|
|
324
|
+
}
|
|
325
|
+
return day;
|
|
326
|
+
})
|
|
327
|
+
: input.values;
|
|
328
|
+
return values.length === 1
|
|
329
|
+
? { field: input.field, op: "eq", value: values[0] }
|
|
330
|
+
: { field: input.field, op: "in", values };
|
|
331
|
+
}
|
|
332
|
+
function normalizeMarketingCohortDimension(value, flag) {
|
|
333
|
+
const aliases = {
|
|
334
|
+
channel: "network",
|
|
335
|
+
country: "country_code",
|
|
336
|
+
"campaign-id": "campaign_id",
|
|
337
|
+
};
|
|
338
|
+
const normalized = aliases[value] ?? value;
|
|
339
|
+
if (!MARKETING_COHORT_DIMENSIONS.has(normalized)) {
|
|
340
|
+
throw new Error(`${flag} dimension must be one of ${[...MARKETING_COHORT_DIMENSIONS].join(", ")}`);
|
|
341
|
+
}
|
|
342
|
+
return normalized;
|
|
343
|
+
}
|
|
344
|
+
function parseMarketingCohortDayOffsets(value) {
|
|
345
|
+
const offsets = new Set();
|
|
346
|
+
for (const part of value.split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
347
|
+
const match = part.match(/^D?(\d+)(?:\s*-\s*D?(\d+))?$/i);
|
|
348
|
+
if (!match)
|
|
349
|
+
throw new Error("--day-offsets must use 0-30 or a comma-separated list such as 0,1,7,14,30");
|
|
350
|
+
const start = Number(match[1]);
|
|
351
|
+
const end = match[2] === undefined ? start : Number(match[2]);
|
|
352
|
+
if (start > end || start < 0 || end > 30) {
|
|
353
|
+
throw new Error("--day-offsets values must be between 0 and 30");
|
|
354
|
+
}
|
|
355
|
+
for (let day = start; day <= end; day += 1)
|
|
356
|
+
offsets.add(day);
|
|
357
|
+
}
|
|
358
|
+
if (!offsets.size)
|
|
359
|
+
throw new Error("--day-offsets requires at least one value");
|
|
360
|
+
return [...offsets].sort((left, right) => left - right);
|
|
361
|
+
}
|
|
362
|
+
function optionalTextFlag(value, flag) {
|
|
363
|
+
if (value === undefined)
|
|
364
|
+
return undefined;
|
|
365
|
+
if (typeof value !== "string" || !value.trim())
|
|
366
|
+
throw new Error(`${flag} requires a value`);
|
|
367
|
+
return value.trim();
|
|
368
|
+
}
|
|
369
|
+
function isIsoDate(value) {
|
|
370
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00.000Z`));
|
|
371
|
+
}
|
|
232
372
|
async function waitForAdvancedReport(client, runId, timeoutMs, startedAt) {
|
|
233
373
|
while (true) {
|
|
234
374
|
const response = await client.getAdvancedReportRun(runId, true);
|
package/package.json
CHANGED
package/skills/gamealgo/SKILL.md
CHANGED
|
@@ -49,9 +49,9 @@ When changing an existing Report Pack, run `gamealgo report pull --out <working-
|
|
|
49
49
|
|
|
50
50
|
Validate with the compact default output, then use `report preview` to inspect v2 validation and generated Dataset SQL without running a warehouse query. After publishing, verify only the affected tab, group, or View with `report result`; do not query unrelated tabs. A report result request must explicitly select a tab, group, or chart/View. A globally unique bare id is accepted; if the Server returns an ambiguity error, use its candidate ids and the local Pack structure to retry without asking the developer to choose an internal 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 and materialized result state.
|
|
51
51
|
|
|
52
|
-
### Use Report Pack v2
|
|
52
|
+
### Use Report Pack v2
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
Every Report Pack must declare `schemaVersion: 2`. v2 uses independent `datasets`, inline `groups[].views`, platform-owned `groups[].standard`, `tabs`, and optional `experimentViews` and `advancedQueries`. It has no top-level `reports` or `charts` and no author-managed Pack version. Generate, explain, validate, preview, and publish only this structure; do not expose migration-era formats to the developer.
|
|
55
55
|
|
|
56
56
|
An ordinary v2 Dataset must read only `adn.dwd_gamealgo_events_enriched_di`, include `dt` in its output grain, and be independently computable for each natural day. Before selector filtering, each `datasetFingerprint × platform × dt` must remain at or below 100 rows and 128 KiB. Estimate the output cardinality from every `groupBy` dimension before publishing. If it may exceed either limit, reduce or bucket dimensions, split the business question, or move it into `advancedQueries`; a table View, front-end selector, or `dataset.limit` does not bypass the ordinary limit.
|
|
57
57
|
|
|
@@ -59,6 +59,8 @@ Keep ratio and average components as additive Dataset columns and calculate the
|
|
|
59
59
|
|
|
60
60
|
Use `gamealgo report result` to read an existing ordinary v2 materialization for an explicit date range and scope. It never starts a DW query. Use `gamealgo report advanced run --query-id <id> --platform <platform> --from <date> --to <date>` only when an explicit high-dimensional or non-preloaded query is required. `--pack <file>` supplies an immutable v2 Pack snapshot for that Run without publishing it. `advanced get` and `advanced list` only read the saved Run/history. Advanced results are whole-run bounded to 20,000 rows and 20 MiB and retained for 30 days; never replace this path with local production scans or a hidden query side channel.
|
|
61
61
|
|
|
62
|
+
For a managed `marketing/lifecycle` advanced Dataset, use runtime cohort flags instead of generating many near-duplicate Packs: `--observation-end <date>`, `--group-by <comma-separated dimensions>`, `--day-offsets <0-30|list>`, `--min-cohort-size <n>`, `--pivot-metric <metric>`, and repeatable `--filter field=value1,value2`. The CLI automatically adds `day_offset` to `--group-by`. Prefer pivoting one requested metric so the 20,000-row check applies after D0–D30 become columns. When the user wants one aggregate matrix for the selected acquisition range, omit `cohort_dt`; include it only when separate daily cohorts are explicitly required.
|
|
63
|
+
|
|
62
64
|
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.
|
|
63
65
|
|
|
64
66
|
## Follow Mandatory Checkpoints
|