Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions docs/agent/asa-metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ No `asa` command takes `--app` to select scope — the token's company already f
The one exception is `--app` as a list filter, and among the four commands here it applies
only to `search-terms list` (the full filter set is in `asa-management.md`).

`metrics` and `metrics overview` take no scope filter at all — no `--app`, `--campaign`,
`--ad-group`, `--search`, `--status`. A call covers the whole account at the `--entity`
level and date window you give it; narrow the answer by matching the returned rows against
`metrics` takes three scope filters and nothing else — `--app`, `--campaign`, `--ad-group`,
all repeatable. There is no `--search` and no `--status`. `metrics overview` takes no scope
filter at all: it always covers the whole account at the `--entity` level and date window you
give it. Where a filter is missing, narrow the answer by matching the returned rows against
ids from a scoped list in `asa-management.md`, not by looking for a flag that isn't there.

`asa-management.md`'s `list` and `get` commands return metadata only — no spend, no ROAS,
Expand All @@ -19,7 +20,7 @@ that field.

| Command | Flags | Notes |
|---|---|---|
| `asa metrics` | `--entity`, `--date-from`, `--date-to` required; `--metric` (repeatable), `--group-by` (repeatable), `--order` (`asc`/`desc`, default `desc`), `--order-by`, `--by-days` (repeatable, max 16), `--order-by-day`, `--page` (default `1`), `--page-size` (default `100`, max `1000`) optional | One row per entity — `ad`, `ad-group`, `campaign`, or `keyword` — already aggregated over the period and already sorted server-side by `--order-by`. A top-N question is one call, `--order-by X --page-size N`; never paginate and sum yourself, and for a full breakdown take one big page (`--page-size 1000`) instead of looping. `--order` defaults to `desc`; pass `--order asc` for a "worst" question instead of "best." `--group-by` is one of `country`, `day`, `month`, `quarter`, `week`, `year`, and its coarseness sets the date-window cap (see [Date window caps](#date-window-caps)). Account-level totals are one call to `metrics overview` instead. |
| `asa metrics` | `--entity`, `--date-from`, `--date-to`, `--metric` (repeatable) required; `--app`, `--campaign`, `--ad-group` (repeatable scope filters), `--group-by` (repeatable), `--order` (`asc`/`desc`, default `desc`), `--order-by`, `--by-days` (repeatable, max 16), `--order-by-day`, `--page` (default `1`), `--page-size` (default `100`, max `1000`) optional | One row per entity — `ad`, `ad-group`, `campaign`, or `keyword` — already aggregated over the period and already sorted server-side by `--order-by`. A top-N question is one call, `--order-by X --page-size N`; never paginate and sum yourself, and for a full breakdown take one big page (`--page-size 1000`) instead of looping. `--order` defaults to `desc`; pass `--order asc` for a "worst" question instead of "best." `--group-by` is one of `country`, `day`, `month`, `quarter`, `week`, `year`, and its coarseness sets the date-window cap (see [Date window caps](#date-window-caps)). Account-level totals are one call to `metrics overview` instead. |
| `asa metrics overview` | `--entity`, `--date-from`, `--date-to` required; `--metric` (repeatable, root names only), `--by-days` (repeatable, max 16), `--period-unit` (`day`/`week`/`month`/`quarter`/`year`, default `day`) optional | Returns one response, not a list — totals for the whole entity level plus a per-period series in the same call, no pagination, no client-side summing. That's the one-call answer to a trend question ("today vs. yesterday," "this week vs. last"). Has no `--group-by` and no `--order`/`--order-by`/`--order-by-day`. Shares the 5-per-minute metrics budget with `metrics` (see [The analytics pool](#the-analytics-pool)). Metric names here are root names only — see [Metric vocabulary](#metric-vocabulary). |
| `asa search-terms list` | `--date-from` / `--date-to` (default: today); scope with `--ad-group` / `--campaign`; `--page` (default `1`), `--page-size` (default `100`, max `1000`) | The only list command in the `asa` topic that takes period flags — it draws on the same analytics pool as `metrics` (see [The analytics pool](#the-analytics-pool)), not the metadata store the other lists use. The full scope-filter set (`--app`, `--campaign-group`, `--search` included) is in `asa-management.md`. This file covers *reading* search terms; turning what you find into keywords or negative keywords is in `asa-management.md`. |
| `asa competitors summary` | `--app-ids` (1–5 Apple App Store IDs, comma-separated) | Covers the last full month across every country — there are deliberately no period or country flags. The first call on a cold cache can take tens of seconds. |
Expand Down Expand Up @@ -61,6 +62,15 @@ applies to it.
one. A wrong name fails the call with an error that lists every valid name, so probing for
names costs at most one call and should never be done on purpose.

`--metric` is **required** on `asa metrics`, and the list is not free: every metric named is
computed across the whole entity level before the page is cut, so name the columns you will
actually read rather than sweeping the catalog. `subscribers` and `paid_subscribers` count
unique profiles per entity and cost about seventeen times a plain spend-and-installs call; the
same applies to `arppu` and `arpas`, which derive from them. Whatever the `--entity`, those
names are refused with `422 cli_metric_scope_too_wide` unless `--campaign` or `--ad-group`
narrows the call. Those two flags (plus `--app`) are also the cheapest way to make any call
fast: cost follows the number of entities aggregated, not the page size.

Cohort roots — `revenue`, `arpu`, `arppu`, `arpas` (alias `cohort_arpas`), `roas`, `roi` —
expand to `gross_`, `proceeds_`, and `net_` variants (`gross_roas`, `proceeds_revenue`,
`net_arpu`, and so on). To rank by a cohort metric with `--order-by`, use the expanded name
Expand Down
32 changes: 25 additions & 7 deletions src/commands/asa/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {Command, Flags} from '@oclif/core'

import {asaWrite, createAsaClient} from '../../../lib/asa-client.js'
import {ASA_GROUP_BY_DIMENSIONS, ASA_METRIC_ENTITIES, asaPaginationFlags, byDaysFlag, MAX_BY_DAYS} from '../../../lib/asa-flags.js'
import {
ASA_GROUP_BY_DIMENSIONS,
ASA_METRIC_ENTITIES,
asaPaginationFlags,
byDaysFlag,
MAX_BY_DAYS,
metricsScopeBody,
metricsScopeFlags,
} from '../../../lib/asa-flags.js'
import {type PaginatedResponse, paginationParams} from '../../../lib/flags.js'
import {printList} from '../../../lib/output.js'

Expand All @@ -15,18 +23,26 @@ day is grouped, 90 with no period grouping, 180 by week, 365 by month and coarse
coarsening the grouping, not by splitting into more calls. Each page is also capped at 5000 breakdown
rows (entities × countries × periods); over it the call fails with 422 cli_response_too_large — coarsen
the grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls per minute, at most
2 per 10 seconds, one at a time.`
2 per 10 seconds, one at a time.

--metric is required and every metric named is computed over the whole entity set, so ask for the
columns you actually read. subscribers and paid_subscribers (and arppu / arpas, which derive from them)
count unique profiles per entity and cost roughly seventeen times the rest; whatever the --entity, they
are refused unless --campaign or --ad-group scopes the call. Those scope flags are also the cheapest way
to make any call fast, since cost follows the number of entities aggregated, not the page size.`
static enableJsonFlag = true
static examples = [
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31',
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --order-by spend --page-size 5',
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --group-by country --page-size 1000',
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric adapty_installs',
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --order-by spend --page-size 5',
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --group-by country --page-size 1000',
'<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric roas',
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90',
'<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric arpas --campaign 0f0e...',
]
static flags = {
...asaPaginationFlags,
...byDaysFlag,
...metricsScopeFlags,
'date-from': Flags.string({description: 'Start of the period (YYYY-MM-DD)', required: true}),
'date-to': Flags.string({description: 'End of the period (YYYY-MM-DD)', required: true}),
entity: Flags.string({description: 'What to report on', options: ASA_METRIC_ENTITIES, required: true}),
Expand All @@ -37,8 +53,9 @@ the grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls p
}),
metric: Flags.string({
description:
'Metric name (dashboard nomenclature, e.g. spend, taps, gross_roas), repeatable; omit for every metric; a wrong name fails listing all valid ones',
'Metric name (dashboard nomenclature, e.g. spend, taps, gross_roas), repeatable and required; every metric asked for is computed over the whole entity set, so list only what you read; a wrong name fails listing all valid ones',
multiple: true,
required: true,
}),
order: Flags.string({default: 'desc', description: 'Sort direction', options: ['asc', 'desc']}),
'order-by': Flags.string({
Expand All @@ -62,8 +79,9 @@ the grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls p
date_from: flags['date-from'],
date_to: flags['date-to'],
entity: flags.entity,
metrics: flags.metric,
order: flags.order,
...(flags.metric === undefined ? {} : {metrics: flags.metric}),
...metricsScopeBody(flags),
...(flags['by-days'] === undefined ? {} : {by_days: flags['by-days']}),
...(flags['group-by'] === undefined ? {} : {group_by: flags['group-by']}),
...(flags['order-by'] === undefined ? {} : {order_by: flags['order-by']}),
Expand Down
14 changes: 14 additions & 0 deletions src/lib/asa-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ export const adScopeFlags = {
...searchFilter,
}

export const metricsScopeFlags = {
'ad-group': idFilter('ad group'),
app: idFilter('app'),
campaign: idFilter('campaign'),
}

export function metricsScopeBody(flags: {'ad-group'?: string[]; app?: string[]; campaign?: string[]}) {
return {
...(flags['ad-group'] === undefined ? {} : {ad_group_id: flags['ad-group']}),
...(flags.app === undefined ? {} : {app_id: flags.app}),
...(flags.campaign === undefined ? {} : {campaign_id: flags.campaign}),
}
}

export const statusFilter = (options: string[]) => ({
status: Flags.string({description: 'Keep only rows in this state', options}),
})
Expand Down
2 changes: 1 addition & 1 deletion test/commands/asa-idempotency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('asa idempotency', () => {

it('metrics posts carry a key too, so a network retry cannot double-submit', async () => {
fetchStub = mockFetch([EMPTY_LIST_RESPONSE])
await runCommand('asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31')
await runCommand('asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend')
expect(keyOf(fetchStub, 0)).to.match(UUID_RE)
})

Expand Down
6 changes: 4 additions & 2 deletions test/commands/asa-writes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,15 @@ describe('asa writes', () => {
expect(body.by_days).to.deep.equal([7, 90])

await runCommand(
'asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --by-days 90 --order-by gross_roas --order-by-day 90',
'asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 90 --order-by gross_roas --order-by-day 90',
)
const ranked = JSON.parse(fetchStub.getCall(1).args[1].body as string)
expect(ranked).to.deep.include({order_by: 'gross_roas', order_by_day: 90})

const byDays = Array.from({length: 17}, (_, index) => `--by-days ${index}`).join(' ')
const {error} = await runCommand(`asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 ${byDays}`)
const {error} = await runCommand(
`asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend ${byDays}`,
)
expect(error?.message).to.contain('At most 16')
expect(fetchStub.callCount).to.equal(2)
})
Expand Down
Loading