adapty 0.8.4-beta.1 → 0.8.4
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 +132 -2
- package/dist/commands/asa/automations/create.d.ts +9 -0
- package/dist/commands/asa/automations/create.js +18 -2
- package/dist/commands/asa/automations/update.d.ts +9 -0
- package/dist/commands/asa/automations/update.js +21 -5
- package/dist/commands/asa/metrics/index.d.ts +4 -1
- package/dist/commands/asa/metrics/index.js +18 -8
- package/dist/lib/asa-flags.d.ts +24 -0
- package/dist/lib/asa-flags.js +54 -0
- package/dist/lib/asa-keyword-action.d.ts +18 -0
- package/dist/lib/asa-keyword-action.js +160 -0
- package/dist/lib/errors.js +2 -1
- package/oclif.manifest.json +274 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,12 +199,142 @@ adapty asa product-pages sync [--adam-id 123456]
|
|
|
199
199
|
|
|
200
200
|
adapty asa automations list
|
|
201
201
|
adapty asa automations get AUTOMATION_ID
|
|
202
|
-
adapty asa automations create --file rule.json [--run-now]
|
|
203
|
-
adapty asa automations update AUTOMATION_ID [--stop] [--start] [--name "..."] [--file rule.json]
|
|
202
|
+
adapty asa automations create --file rule.json [--run-now] [--target-ad-group UUID ...]
|
|
203
|
+
adapty asa automations update AUTOMATION_ID [--stop] [--start] [--name "..."] [--file rule.json] [--target-ad-group UUID ...]
|
|
204
204
|
adapty asa automations run AUTOMATION_ID [--dry-run]
|
|
205
205
|
adapty asa automations runs AUTOMATION_ID
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
A rule file carries the whole rule: `name`, `status` (1 active, 0 stopped), `operate_with` (what the
|
|
209
|
+
rule iterates over), `apply_to` (where it looks), exactly one condition, exactly one action and a
|
|
210
|
+
`run_frequency`. The API stores one action and one condition per rule and rejects anything else.
|
|
211
|
+
|
|
212
|
+
| Field | Shape |
|
|
213
|
+
| --------------- | ------------------------------------------------------------------------------------------- |
|
|
214
|
+
| `operate_with` | `search-term`, `targeting-keyword`, `campaign`, `ad-group` |
|
|
215
|
+
| `apply_to[]` | `{"internal_id": UUID, "type": "campaign-group" \| "app" \| "campaign" \| "ad-group" \| "targeting-keywords"}` |
|
|
216
|
+
| `conditions[0]` | `{"operator": ..., "args": number, "operand": {...}}`, or `{"operator": "and" \| "or", "args": [condition, ...]}` |
|
|
217
|
+
| `operator` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` for a leaf; `and`, `or` to nest |
|
|
218
|
+
| `operand.field` | a metric name in dashboard nomenclature — the same vocabulary `asa metrics --metric` takes |
|
|
219
|
+
| `date_range_type` | `today`, `yesterday`, `last_1_d`, `last_3_d`, `last_7_d`, `last_14_d`, `last_28_d`, `last_30_d`, `last_60_d`, `last_90_d`, `custom` |
|
|
220
|
+
| `run_frequency` | `{"type": "daily", "hour": 8}`, `{"type": "hour", "value": 24, "start_time": 8}`, `{"type": "weekly", "weekdays": ["monday"], "hour": 8}`, `{"type": "monthly", "days": [1], "hour": 8}`, `{"type": "once", "date_time": "2026-10-01T09:00:00Z"}` — `hour` and `start_time` are UTC |
|
|
221
|
+
|
|
222
|
+
The `add-as-keyword-to` action promotes what the rule found into a keyword in one or more target ad
|
|
223
|
+
groups. Its `params` differ by `operate_with`, and the API picks the variant by shape without a
|
|
224
|
+
discriminator: a key that belongs to another variant makes it silently choose that variant and drop
|
|
225
|
+
the rest, which is how a rule ends up as "Add as keyword to 0 ad groups". `negate` and
|
|
226
|
+
`skip_enable_duplicate_keywords` exist only on a `search-term` rule, `pause_in_original_ad_group`
|
|
227
|
+
only on a `targeting-keyword` one, and `targets` holds `internal_ids` — never `ids`.
|
|
228
|
+
|
|
229
|
+
A full `search-term` rule — every term with 10+ taps over the last week becomes an exact keyword in
|
|
230
|
+
the target ad group at the term's own CPT, and is negated in the ad group it came from:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"name": "Search term harvester",
|
|
235
|
+
"status": 1,
|
|
236
|
+
"operate_with": "search-term",
|
|
237
|
+
"apply_to": [{"internal_id": "CAMPAIGN_UUID", "type": "campaign"}],
|
|
238
|
+
"conditions": [
|
|
239
|
+
{
|
|
240
|
+
"operator": "gte",
|
|
241
|
+
"args": 10,
|
|
242
|
+
"operand": {
|
|
243
|
+
"field": "taps",
|
|
244
|
+
"field_type": "base_field",
|
|
245
|
+
"date_range_type": "last_7_d",
|
|
246
|
+
"date_range_size": 0,
|
|
247
|
+
"date_range_offset": 0,
|
|
248
|
+
"by_days": null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
"actions": [
|
|
253
|
+
{
|
|
254
|
+
"type": "add-as-keyword-to",
|
|
255
|
+
"params": {
|
|
256
|
+
"targets": {"type": "ad-group", "internal_ids": ["AD_GROUP_UUID"]},
|
|
257
|
+
"cpt_bid": {"type": "search_term_current_cpt", "value": null},
|
|
258
|
+
"match_type": "EXACT",
|
|
259
|
+
"negate": {"enabled": true, "type": "ad-group"},
|
|
260
|
+
"skip_enable_duplicate_keywords": false
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
"run_frequency": {"type": "daily", "hour": 8}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The same action on a `targeting-keyword` rule — a broad keyword that has earned installs graduates
|
|
269
|
+
into the exact ad group at its current bid and is paused where it was:
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"name": "Graduate broad keywords to exact",
|
|
274
|
+
"status": 1,
|
|
275
|
+
"operate_with": "targeting-keyword",
|
|
276
|
+
"apply_to": [{"internal_id": "SOURCE_AD_GROUP_UUID", "type": "ad-group"}],
|
|
277
|
+
"conditions": [
|
|
278
|
+
{
|
|
279
|
+
"operator": "gte",
|
|
280
|
+
"args": 3,
|
|
281
|
+
"operand": {
|
|
282
|
+
"field": "total_installs",
|
|
283
|
+
"field_type": "base_field",
|
|
284
|
+
"date_range_type": "last_14_d",
|
|
285
|
+
"date_range_size": 0,
|
|
286
|
+
"date_range_offset": 0,
|
|
287
|
+
"by_days": null
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"actions": [
|
|
292
|
+
{
|
|
293
|
+
"type": "add-as-keyword-to",
|
|
294
|
+
"params": {
|
|
295
|
+
"targets": {"type": "ad-group", "internal_ids": ["EXACT_AD_GROUP_UUID"]},
|
|
296
|
+
"cpt_bid": {"type": "keyword_current_bid", "value": null},
|
|
297
|
+
"match_type": "EXACT",
|
|
298
|
+
"pause_in_original_ad_group": true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
"run_frequency": {"type": "daily", "hour": 8}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Rather than hand-writing that `params` block, pass the action flags — they fill in or override
|
|
307
|
+
`actions[0].params` in the file, and the CLI refuses a rule the API would have quietly accepted and
|
|
308
|
+
broken:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
adapty asa automations create --file rule.json --target-ad-group AD_GROUP_UUID \
|
|
312
|
+
--match-type EXACT --cpt-bid-type search_term_current_cpt --negate ad-group
|
|
313
|
+
adapty asa automations create --file rule.json --target-ad-group AD_GROUP_UUID \
|
|
314
|
+
--match-type BROAD --cpt-bid-type set_to --cpt-bid 1.50 --no-negate --skip-enable-duplicates
|
|
315
|
+
adapty asa automations update AUTOMATION_ID --target-ad-group AD_GROUP_UUID \
|
|
316
|
+
--match-type EXACT --cpt-bid-type search_term_current_cpt
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
| Flag | Where it lands |
|
|
320
|
+
| -------------------------- | --------------------------------------------------------------------- |
|
|
321
|
+
| `--target-ad-group` | `targets.internal_ids`, repeatable; a rule without one does nothing |
|
|
322
|
+
| `--match-type` | `match_type`: `BROAD` or `EXACT` |
|
|
323
|
+
| `--cpt-bid-type` | `cpt_bid.type`: `ad_group_default_bid`, `set_to`, `search_term_current_cpt`, `keyword_current_bid` |
|
|
324
|
+
| `--cpt-bid` | `cpt_bid.value`: the bid itself with `set_to` (required), a percent markup on the entity's own bid with `search_term_current_cpt` / `keyword_current_bid`, rejected with `ad_group_default_bid` |
|
|
325
|
+
| `--negate` / `--no-negate` | `negate` (search-term rules): `ad-group` or `campaign`, or off |
|
|
326
|
+
| `--skip-enable-duplicates` | `skip_enable_duplicate_keywords` (search-term rules) |
|
|
327
|
+
| `--pause-original` | `pause_in_original_ad_group` (targeting-keyword rules) |
|
|
328
|
+
|
|
329
|
+
`--cpt-bid-type` and `--match-type` have no defaults, here or in the API: a bid and a match type are
|
|
330
|
+
a spend decision and a reach decision, so when `params` are built from scratch the CLI asks for them
|
|
331
|
+
instead of guessing. On `update`, an action flag turns the call into a read-modify-write — the rule
|
|
332
|
+
is read, `actions[0].params` is rebuilt and the whole `actions` list is written back, since the API
|
|
333
|
+
replaces it wholesale. That also repairs a rule whose stored `params` carry the wrong shape: only
|
|
334
|
+
the keys that fit are kept, the strays are dropped, and anything missing has to come from a flag. A
|
|
335
|
+
dashboard edit made between the read and the write is overwritten.
|
|
336
|
+
|
|
337
|
+
|
|
208
338
|
Metrics take an entity level, a period and an optional metric selection. Rows come back one per entity,
|
|
209
339
|
aggregated and sorted server-side, so a top-N or a breakdown is a single call — use `--order-by` with a small
|
|
210
340
|
`--page-size` for rankings, `metrics overview` for account totals and time series, and one big page (up to
|
|
@@ -9,8 +9,17 @@ export default class AsaAutomationsCreate extends Command {
|
|
|
9
9
|
'run-now': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'cpt-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'cpt-bid-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'match-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
negate: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'no-negate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
'pause-original': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
'skip-enable-duplicates': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
'target-ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
20
|
};
|
|
13
21
|
run(): Promise<AsaAutomationMutationDTO>;
|
|
22
|
+
private applyActionFlags;
|
|
14
23
|
private readRule;
|
|
15
24
|
private readStdin;
|
|
16
25
|
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
|
|
4
|
-
import { idempotencyFlags } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { addKeywordActionFlags, idempotencyFlags } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { hasAddKeywordActionFlags, rebuildAddKeywordAction, requireSingleAction, } from '../../../lib/asa-keyword-action.js';
|
|
5
6
|
import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
|
|
6
7
|
import { printResponse } from '../../../lib/output.js';
|
|
7
8
|
export default class AsaAutomationsCreate extends Command {
|
|
8
|
-
static description = 'Create an automation rule from a JSON rule file';
|
|
9
|
+
static description = 'Create an automation rule from a JSON rule file. The add-as-keyword action flags fill in or override actions[0].params, so the bid, the match type and the target ad groups need no hand-written JSON.';
|
|
9
10
|
static enableJsonFlag = true;
|
|
10
11
|
static examples = [
|
|
11
12
|
'<%= config.bin %> asa automations create --file rule.json',
|
|
12
13
|
'<%= config.bin %> asa automations create --file rule.json --run-now',
|
|
14
|
+
'<%= config.bin %> asa automations create --file rule.json --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type search_term_current_cpt',
|
|
15
|
+
'<%= config.bin %> asa automations create --file rule.json --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type set_to --cpt-bid 1.50 --negate ad-group',
|
|
13
16
|
];
|
|
14
17
|
static flags = {
|
|
18
|
+
...addKeywordActionFlags,
|
|
15
19
|
...confirmFlags,
|
|
16
20
|
...idempotencyFlags,
|
|
17
21
|
file: Flags.string({ description: 'JSON file with the rule body, or - to read stdin', required: true }),
|
|
@@ -22,6 +26,7 @@ export default class AsaAutomationsCreate extends Command {
|
|
|
22
26
|
const body = await this.readRule(flags.file);
|
|
23
27
|
if (flags['run-now'])
|
|
24
28
|
body.run_immediately = true;
|
|
29
|
+
this.applyActionFlags(body, flags);
|
|
25
30
|
const summary = flags['run-now'] ? 'Create automation rule and run it immediately' : 'Create automation rule';
|
|
26
31
|
await confirmMutation(this, { body, method: 'POST', path: '/automations/', summary }, flags.yes);
|
|
27
32
|
const client = await createAsaClient(this.config);
|
|
@@ -36,6 +41,17 @@ export default class AsaAutomationsCreate extends Command {
|
|
|
36
41
|
printResponse(result, this.log.bind(this));
|
|
37
42
|
return result;
|
|
38
43
|
}
|
|
44
|
+
applyActionFlags(body, flags) {
|
|
45
|
+
try {
|
|
46
|
+
requireSingleAction(body.actions);
|
|
47
|
+
if (!hasAddKeywordActionFlags(flags))
|
|
48
|
+
return;
|
|
49
|
+
body.actions = rebuildAddKeywordAction(body.actions, body.operate_with, flags);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: 2 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
39
55
|
async readRule(path) {
|
|
40
56
|
let raw;
|
|
41
57
|
try {
|
|
@@ -14,8 +14,17 @@ export default class AsaAutomationsUpdate extends Command {
|
|
|
14
14
|
stop: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
16
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
'cpt-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
'cpt-bid-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
'match-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
negate: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
'no-negate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
+
'pause-original': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
'skip-enable-duplicates': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
+
'target-ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
25
|
};
|
|
18
26
|
run(): Promise<AsaAutomationMutationDTO>;
|
|
19
27
|
private readRule;
|
|
20
28
|
private readStdin;
|
|
29
|
+
private rebuildAction;
|
|
21
30
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { asaWrite, createAsaClient, noteReplay } from '../../../lib/asa-client.js';
|
|
4
|
-
import { idempotencyFlags } from '../../../lib/asa-flags.js';
|
|
4
|
+
import { addKeywordActionFlags, idempotencyFlags } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { hasAddKeywordActionFlags, rebuildAddKeywordAction, } from '../../../lib/asa-keyword-action.js';
|
|
5
6
|
import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
|
|
6
7
|
import { isValidUuid } from '../../../lib/flags.js';
|
|
7
8
|
import { printResponse } from '../../../lib/output.js';
|
|
@@ -9,13 +10,15 @@ export default class AsaAutomationsUpdate extends Command {
|
|
|
9
10
|
static args = {
|
|
10
11
|
automation_id: Args.string({ description: 'Automation rule ID (UUID)', required: true }),
|
|
11
12
|
};
|
|
12
|
-
static description = 'Change an automation rule: stop it, rename it, or replace parts of the rule';
|
|
13
|
+
static description = 'Change an automation rule: stop it, rename it, or replace parts of the rule. An add-as-keyword action flag turns the call into a read-modify-write: the rule is read, actions[0].params is rebuilt from the flags and the whole actions list is written back — an edit someone makes in the dashboard in between is overwritten. This is also the way to repair a rule whose stored params carry the wrong shape.';
|
|
13
14
|
static enableJsonFlag = true;
|
|
14
15
|
static examples = [
|
|
15
16
|
'<%= config.bin %> asa automations update UUID --stop',
|
|
16
17
|
'<%= config.bin %> asa automations update UUID --file rule.json',
|
|
18
|
+
'<%= config.bin %> asa automations update UUID --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type search_term_current_cpt',
|
|
17
19
|
];
|
|
18
20
|
static flags = {
|
|
21
|
+
...addKeywordActionFlags,
|
|
19
22
|
...confirmFlags,
|
|
20
23
|
...idempotencyFlags,
|
|
21
24
|
file: Flags.string({ description: 'JSON file with the parts to change, or - to read stdin' }),
|
|
@@ -34,14 +37,17 @@ export default class AsaAutomationsUpdate extends Command {
|
|
|
34
37
|
body.status = 1;
|
|
35
38
|
if (flags.stop)
|
|
36
39
|
body.status = 0;
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const actionFlags = hasAddKeywordActionFlags(flags);
|
|
41
|
+
if (Object.keys(body).length === 0 && !actionFlags) {
|
|
42
|
+
this.error('Nothing to change. Pass --stop, --start, --name, --file or an action flag.', { exit: 2 });
|
|
39
43
|
}
|
|
40
44
|
if ('internal_id' in body) {
|
|
41
45
|
this.error('Remove internal_id from the file: the rule ID comes from the command line.', { exit: 2 });
|
|
42
46
|
}
|
|
43
|
-
await confirmMutation(this, { body, method: 'PUT', path: `/automations/${args.automation_id}/`, summary: 'Update automation rule' }, flags.yes);
|
|
44
47
|
const client = await createAsaClient(this.config);
|
|
48
|
+
if (actionFlags)
|
|
49
|
+
await this.rebuildAction(client, args.automation_id, body, flags);
|
|
50
|
+
await confirmMutation(this, { body, method: 'PUT', path: `/automations/${args.automation_id}/`, summary: 'Update automation rule' }, flags.yes);
|
|
45
51
|
const { replayed, result } = await asaWrite(client, 'put', `/automations/${args.automation_id}`, { body, idempotencyKey: flags['idempotency-key'] });
|
|
46
52
|
noteReplay(replayed, this.log.bind(this));
|
|
47
53
|
if (result.automation && !replayed)
|
|
@@ -70,4 +76,14 @@ export default class AsaAutomationsUpdate extends Command {
|
|
|
70
76
|
chunks.push(chunk);
|
|
71
77
|
return Buffer.concat(chunks).toString('utf8');
|
|
72
78
|
}
|
|
79
|
+
// Read-modify-write: the API replaces `actions` wholesale, so the whole list has to be sent back.
|
|
80
|
+
async rebuildAction(client, automationId, body, flags) {
|
|
81
|
+
const rule = await client.get(`/automations/${automationId}`);
|
|
82
|
+
try {
|
|
83
|
+
body.actions = rebuildAddKeywordAction(body.actions ?? rule.actions, body.operate_with ?? rule.operate_with, flags);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: 2 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
73
89
|
}
|
|
@@ -9,10 +9,13 @@ export default class AsaMetrics extends Command {
|
|
|
9
9
|
'date-to': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
entity: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
'group-by': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
metric: import("@oclif/core/interfaces").OptionFlag<string[]
|
|
12
|
+
metric: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
order: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
14
|
'order-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
15
|
'order-by-day': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
19
|
'by-days': import("@oclif/core/interfaces").OptionFlag<number[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
20
|
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
21
|
'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import { asaWrite, createAsaClient } from '../../../lib/asa-client.js';
|
|
3
|
-
import { ASA_GROUP_BY_DIMENSIONS, ASA_METRIC_ENTITIES, asaPaginationFlags, byDaysFlag, MAX_BY_DAYS } from '../../../lib/asa-flags.js';
|
|
3
|
+
import { ASA_GROUP_BY_DIMENSIONS, ASA_METRIC_ENTITIES, asaPaginationFlags, byDaysFlag, MAX_BY_DAYS, metricsScopeBody, metricsScopeFlags, } from '../../../lib/asa-flags.js';
|
|
4
4
|
import { paginationParams } from '../../../lib/flags.js';
|
|
5
5
|
import { printList } from '../../../lib/output.js';
|
|
6
6
|
export default class AsaMetrics extends Command {
|
|
@@ -10,21 +10,29 @@ One row per entity, already aggregated server-side and sorted by --order-by, so
|
|
|
10
10
|
call with --order-by and --page-size N — never sum pages yourself. Account-level totals are one call to
|
|
11
11
|
asa metrics overview instead. The date window is capped by the finest --group-by period: 28 days when
|
|
12
12
|
day is grouped, 90 with no period grouping, 180 by week, 365 by month and coarser — widen the window by
|
|
13
|
-
coarsening the grouping, not by splitting into more calls. Each page is also capped at
|
|
13
|
+
coarsening the grouping, not by splitting into more calls. Each page is also capped at 5000 breakdown
|
|
14
14
|
rows (entities × countries × periods); over it the call fails with 422 cli_response_too_large — coarsen
|
|
15
15
|
the grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls per minute, at most
|
|
16
|
-
2 per 10 seconds, one at a time
|
|
16
|
+
2 per 10 seconds, one at a time.
|
|
17
|
+
|
|
18
|
+
--metric is required and every metric named is computed over the whole entity set, so ask for the
|
|
19
|
+
columns you actually read. subscribers and paid_subscribers (and arppu / arpas, which derive from them)
|
|
20
|
+
count unique profiles per entity and cost roughly seventeen times the rest; whatever the --entity, they
|
|
21
|
+
are refused unless --campaign or --ad-group scopes the call. Those scope flags are also the cheapest way
|
|
22
|
+
to make any call fast, since cost follows the number of entities aggregated, not the page size.`;
|
|
17
23
|
static enableJsonFlag = true;
|
|
18
24
|
static examples = [
|
|
19
|
-
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31',
|
|
20
|
-
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --order-by spend --page-size 5',
|
|
21
|
-
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --group-by country --page-size 1000',
|
|
25
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric adapty_installs',
|
|
26
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --order-by spend --page-size 5',
|
|
27
|
+
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --group-by country --page-size 1000',
|
|
22
28
|
'<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric roas',
|
|
23
29
|
'<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90',
|
|
30
|
+
'<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric arpas --campaign 0f0e...',
|
|
24
31
|
];
|
|
25
32
|
static flags = {
|
|
26
33
|
...asaPaginationFlags,
|
|
27
34
|
...byDaysFlag,
|
|
35
|
+
...metricsScopeFlags,
|
|
28
36
|
'date-from': Flags.string({ description: 'Start of the period (YYYY-MM-DD)', required: true }),
|
|
29
37
|
'date-to': Flags.string({ description: 'End of the period (YYYY-MM-DD)', required: true }),
|
|
30
38
|
entity: Flags.string({ description: 'What to report on', options: ASA_METRIC_ENTITIES, required: true }),
|
|
@@ -34,8 +42,9 @@ the grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls p
|
|
|
34
42
|
options: ASA_GROUP_BY_DIMENSIONS,
|
|
35
43
|
}),
|
|
36
44
|
metric: Flags.string({
|
|
37
|
-
description: 'Metric name (dashboard nomenclature, e.g. spend, taps, gross_roas), repeatable;
|
|
45
|
+
description: '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',
|
|
38
46
|
multiple: true,
|
|
47
|
+
required: true,
|
|
39
48
|
}),
|
|
40
49
|
order: Flags.string({ default: 'desc', description: 'Sort direction', options: ['asc', 'desc'] }),
|
|
41
50
|
'order-by': Flags.string({
|
|
@@ -56,8 +65,9 @@ the grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls p
|
|
|
56
65
|
date_from: flags['date-from'],
|
|
57
66
|
date_to: flags['date-to'],
|
|
58
67
|
entity: flags.entity,
|
|
68
|
+
metrics: flags.metric,
|
|
59
69
|
order: flags.order,
|
|
60
|
-
...(flags
|
|
70
|
+
...metricsScopeBody(flags),
|
|
61
71
|
...(flags['by-days'] === undefined ? {} : { by_days: flags['by-days'] }),
|
|
62
72
|
...(flags['group-by'] === undefined ? {} : { group_by: flags['group-by'] }),
|
|
63
73
|
...(flags['order-by'] === undefined ? {} : { order_by: flags['order-by'] }),
|
package/dist/lib/asa-flags.d.ts
CHANGED
|
@@ -44,6 +44,20 @@ export declare const adScopeFlags: {
|
|
|
44
44
|
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
45
|
'campaign-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
46
46
|
};
|
|
47
|
+
export declare const metricsScopeFlags: {
|
|
48
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
49
|
+
app: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
50
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
51
|
+
};
|
|
52
|
+
export declare function metricsScopeBody(flags: {
|
|
53
|
+
'ad-group'?: string[];
|
|
54
|
+
app?: string[];
|
|
55
|
+
campaign?: string[];
|
|
56
|
+
}): {
|
|
57
|
+
campaign_id?: string[] | undefined;
|
|
58
|
+
app_id?: string[] | undefined;
|
|
59
|
+
ad_group_id?: string[] | undefined;
|
|
60
|
+
};
|
|
47
61
|
export declare const statusFilter: (options: string[]) => {
|
|
48
62
|
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
49
63
|
};
|
|
@@ -75,6 +89,16 @@ export declare function moneyFlag(description: string, opts?: {
|
|
|
75
89
|
export declare const currencyFlag: {
|
|
76
90
|
currency: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
77
91
|
};
|
|
92
|
+
export declare const addKeywordActionFlags: {
|
|
93
|
+
'cpt-bid': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
94
|
+
'cpt-bid-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
95
|
+
'match-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
96
|
+
negate: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
97
|
+
'no-negate': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
98
|
+
'pause-original': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
99
|
+
'skip-enable-duplicates': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
100
|
+
'target-ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
101
|
+
};
|
|
78
102
|
export declare const idempotencyFlags: {
|
|
79
103
|
'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
80
104
|
};
|
package/dist/lib/asa-flags.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { CPT_BID_TYPES, KEYWORD_MATCH_TYPES, NEGATE_TYPES } from './asa-keyword-action.js';
|
|
2
3
|
import { describeListedError } from './errors.js';
|
|
3
4
|
import { isValidUuid } from './flags.js';
|
|
4
5
|
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -63,6 +64,18 @@ export const adScopeFlags = {
|
|
|
63
64
|
'campaign-group': idFilter('campaign group'),
|
|
64
65
|
...searchFilter,
|
|
65
66
|
};
|
|
67
|
+
export const metricsScopeFlags = {
|
|
68
|
+
'ad-group': idFilter('ad group'),
|
|
69
|
+
app: idFilter('app'),
|
|
70
|
+
campaign: idFilter('campaign'),
|
|
71
|
+
};
|
|
72
|
+
export function metricsScopeBody(flags) {
|
|
73
|
+
return {
|
|
74
|
+
...(flags['ad-group'] === undefined ? {} : { ad_group_id: flags['ad-group'] }),
|
|
75
|
+
...(flags.app === undefined ? {} : { app_id: flags.app }),
|
|
76
|
+
...(flags.campaign === undefined ? {} : { campaign_id: flags.campaign }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
66
79
|
export const statusFilter = (options) => ({
|
|
67
80
|
status: Flags.string({ description: 'Keep only rows in this state', options }),
|
|
68
81
|
});
|
|
@@ -116,6 +129,47 @@ export function moneyFlag(description, opts = {}) {
|
|
|
116
129
|
export const currencyFlag = {
|
|
117
130
|
currency: Flags.string({ default: 'USD', description: 'Currency code for the amounts in this call' }),
|
|
118
131
|
};
|
|
132
|
+
// Params of the add-as-keyword-to action, shared by `automations create` and `automations update`.
|
|
133
|
+
// The API picks the params variant by shape, so which of these apply depends on the rule's
|
|
134
|
+
// operate_with: --negate/--no-negate/--skip-enable-duplicates are search-term only,
|
|
135
|
+
// --pause-original is targeting-keyword only. See lib/asa-keyword-action.ts.
|
|
136
|
+
export const addKeywordActionFlags = {
|
|
137
|
+
'cpt-bid': Flags.string({
|
|
138
|
+
description: 'cpt_bid.value: the bid itself with --cpt-bid-type set_to, or a percent markup on the entity bid with ' +
|
|
139
|
+
'search_term_current_cpt / keyword_current_bid; not accepted with ad_group_default_bid',
|
|
140
|
+
parse: parseMoney,
|
|
141
|
+
}),
|
|
142
|
+
'cpt-bid-type': Flags.string({
|
|
143
|
+
description: 'Where the bid of the created keyword comes from; the API has no default',
|
|
144
|
+
options: CPT_BID_TYPES,
|
|
145
|
+
}),
|
|
146
|
+
'match-type': Flags.string({
|
|
147
|
+
description: 'Match type of the created keyword; the API has no default',
|
|
148
|
+
options: KEYWORD_MATCH_TYPES,
|
|
149
|
+
}),
|
|
150
|
+
negate: Flags.string({
|
|
151
|
+
description: 'Also add the search term as a negative keyword at this level (search-term rules)',
|
|
152
|
+
exclusive: ['no-negate'],
|
|
153
|
+
options: NEGATE_TYPES,
|
|
154
|
+
}),
|
|
155
|
+
'no-negate': Flags.boolean({
|
|
156
|
+
description: 'Leave no negative keyword behind (search-term rules)',
|
|
157
|
+
exclusive: ['negate'],
|
|
158
|
+
}),
|
|
159
|
+
'pause-original': Flags.boolean({
|
|
160
|
+
allowNo: true,
|
|
161
|
+
description: 'Pause the source keyword in its own ad group (targeting-keyword rules)',
|
|
162
|
+
}),
|
|
163
|
+
'skip-enable-duplicates': Flags.boolean({
|
|
164
|
+
allowNo: true,
|
|
165
|
+
description: 'Skip a keyword that already exists in the target ad group instead of enabling it (search-term rules)',
|
|
166
|
+
}),
|
|
167
|
+
'target-ad-group': Flags.string({
|
|
168
|
+
description: 'Ad group the keyword is added to (UUID), repeatable; a rule without one does nothing',
|
|
169
|
+
multiple: true,
|
|
170
|
+
parse: parseId,
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
119
173
|
export const idempotencyFlags = {
|
|
120
174
|
'idempotency-key': Flags.string({
|
|
121
175
|
description: 'Idempotency key for this write; re-running with the same key replays the stored result instead of applying twice',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const ADD_AS_KEYWORD_ACTION = "add-as-keyword-to";
|
|
2
|
+
export declare const CPT_BID_TYPES: string[];
|
|
3
|
+
export declare const KEYWORD_MATCH_TYPES: string[];
|
|
4
|
+
export declare const NEGATE_TYPES: string[];
|
|
5
|
+
export interface AddKeywordActionFlags {
|
|
6
|
+
'cpt-bid'?: string;
|
|
7
|
+
'cpt-bid-type'?: string;
|
|
8
|
+
'match-type'?: string;
|
|
9
|
+
negate?: string;
|
|
10
|
+
'no-negate'?: boolean;
|
|
11
|
+
'pause-original'?: boolean;
|
|
12
|
+
'skip-enable-duplicates'?: boolean;
|
|
13
|
+
'target-ad-group'?: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function hasAddKeywordActionFlags(flags: AddKeywordActionFlags): boolean;
|
|
16
|
+
export declare function buildAddKeywordParams(operateWith: unknown, stored: unknown, flags: AddKeywordActionFlags): Record<string, unknown>;
|
|
17
|
+
export declare function requireSingleAction(actions: unknown): Record<string, unknown>;
|
|
18
|
+
export declare function rebuildAddKeywordAction(actions: unknown, operateWith: unknown, flags: AddKeywordActionFlags): Record<string, unknown>[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { isValidUuid } from './flags.js';
|
|
2
|
+
export const ADD_AS_KEYWORD_ACTION = 'add-as-keyword-to';
|
|
3
|
+
export const CPT_BID_TYPES = ['ad_group_default_bid', 'keyword_current_bid', 'search_term_current_cpt', 'set_to'];
|
|
4
|
+
export const KEYWORD_MATCH_TYPES = ['BROAD', 'EXACT'];
|
|
5
|
+
export const NEGATE_TYPES = ['ad-group', 'campaign'];
|
|
6
|
+
const SEARCH_TERM = 'search-term';
|
|
7
|
+
const TARGETING_KEYWORD = 'targeting-keyword';
|
|
8
|
+
const BID_TYPE_IGNORING_VALUE = 'ad_group_default_bid';
|
|
9
|
+
const BID_TYPES_READING_MARKUP = new Set(['keyword_current_bid', 'search_term_current_cpt']);
|
|
10
|
+
// KeywordMatchType also carries AUTO. --match-type never offers it — an automation creates a
|
|
11
|
+
// keyword, and AUTO is not something a keyword can be — but a rule that already stores AUTO keeps
|
|
12
|
+
// it rather than being pushed onto another match type by an edit that never mentioned match types.
|
|
13
|
+
const STORED_MATCH_TYPES = new Set(['AUTO', ...KEYWORD_MATCH_TYPES]);
|
|
14
|
+
const SHARED_FLAGS = ['target-ad-group', 'match-type', 'cpt-bid-type', 'cpt-bid'];
|
|
15
|
+
const SEARCH_TERM_FLAGS = ['negate', 'no-negate', 'skip-enable-duplicates'];
|
|
16
|
+
const TARGETING_KEYWORD_FLAGS = ['pause-original'];
|
|
17
|
+
const ALL_FLAGS = [...SHARED_FLAGS, ...SEARCH_TERM_FLAGS, ...TARGETING_KEYWORD_FLAGS];
|
|
18
|
+
const MISSING_REASONS = {
|
|
19
|
+
'cpt-bid': 'the bid to set, required by --cpt-bid-type set_to',
|
|
20
|
+
'cpt-bid-type': 'where the bid of the new keyword comes from; the API has no default',
|
|
21
|
+
'match-type': 'BROAD or EXACT; the API has no default',
|
|
22
|
+
'target-ad-group': 'the ad groups the keyword is added to; a rule with none does nothing',
|
|
23
|
+
};
|
|
24
|
+
function dashed(name) {
|
|
25
|
+
return `--${name}`;
|
|
26
|
+
}
|
|
27
|
+
function asRecord(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {};
|
|
29
|
+
}
|
|
30
|
+
function isFlagSet(flags, name) {
|
|
31
|
+
const value = flags[name];
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return false;
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return value.length > 0;
|
|
36
|
+
// --no-negate has no negated form of its own, so only a true means it was passed.
|
|
37
|
+
return name === 'no-negate' ? value === true : true;
|
|
38
|
+
}
|
|
39
|
+
export function hasAddKeywordActionFlags(flags) {
|
|
40
|
+
return ALL_FLAGS.some((name) => isFlagSet(flags, name));
|
|
41
|
+
}
|
|
42
|
+
// Params are rebuilt, not patched: a rule may store the add-as-negative-keyword shape
|
|
43
|
+
// ({target_type, ids}) under an add-as-keyword-to action, and the API picks the params variant by
|
|
44
|
+
// shape without a discriminator — so every stray key silently changes which action gets run.
|
|
45
|
+
// Only what fits the expected shape is carried over; the rest has to come from the flags.
|
|
46
|
+
function storedInternalIds(params) {
|
|
47
|
+
const ids = asRecord(params.targets).internal_ids;
|
|
48
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === 'string' && isValidUuid(id)) : [];
|
|
49
|
+
}
|
|
50
|
+
function storedMatchType(params) {
|
|
51
|
+
const matchType = params.match_type;
|
|
52
|
+
return typeof matchType === 'string' && STORED_MATCH_TYPES.has(matchType) ? matchType : undefined;
|
|
53
|
+
}
|
|
54
|
+
function storedCptBidType(params) {
|
|
55
|
+
const { type } = asRecord(params.cpt_bid);
|
|
56
|
+
return typeof type === 'string' && CPT_BID_TYPES.includes(type) ? type : undefined;
|
|
57
|
+
}
|
|
58
|
+
function storedCptBidValue(params) {
|
|
59
|
+
const { value } = asRecord(params.cpt_bid);
|
|
60
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
function storedBoolean(value) {
|
|
63
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
64
|
+
}
|
|
65
|
+
function storedNegate(params) {
|
|
66
|
+
const { enabled, type } = asRecord(params.negate);
|
|
67
|
+
if (typeof enabled !== 'boolean')
|
|
68
|
+
return undefined;
|
|
69
|
+
if (type === null || type === undefined)
|
|
70
|
+
return { enabled, type: null };
|
|
71
|
+
return typeof type === 'string' && NEGATE_TYPES.includes(type) ? { enabled, type } : undefined;
|
|
72
|
+
}
|
|
73
|
+
function resolveNegate(flags, params) {
|
|
74
|
+
if (flags.negate !== undefined)
|
|
75
|
+
return { enabled: true, type: flags.negate };
|
|
76
|
+
if (flags['no-negate'] === true)
|
|
77
|
+
return { enabled: false, type: null };
|
|
78
|
+
return storedNegate(params) ?? { enabled: false, type: null };
|
|
79
|
+
}
|
|
80
|
+
function resolveCptBidValue(cptBidType, bid, params) {
|
|
81
|
+
if (cptBidType === 'set_to')
|
|
82
|
+
return bid === undefined ? storedCptBidValue(params) : Number(bid);
|
|
83
|
+
if (cptBidType !== undefined && BID_TYPES_READING_MARKUP.has(cptBidType)) {
|
|
84
|
+
return bid === undefined ? (storedCptBidValue(params) ?? null) : Number(bid);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function isSearchTermRule(operateWith) {
|
|
89
|
+
if (operateWith === SEARCH_TERM)
|
|
90
|
+
return true;
|
|
91
|
+
if (operateWith === TARGETING_KEYWORD)
|
|
92
|
+
return false;
|
|
93
|
+
const shown = typeof operateWith === 'string' && operateWith.length > 0 ? `"${operateWith}"` : 'missing';
|
|
94
|
+
throw new Error(`${ADD_AS_KEYWORD_ACTION} runs on a "${SEARCH_TERM}" or "${TARGETING_KEYWORD}" rule only; this rule's operate_with is ${shown}.`);
|
|
95
|
+
}
|
|
96
|
+
function rejectForeignFlags(searchTerm, flags) {
|
|
97
|
+
const foreign = (searchTerm ? TARGETING_KEYWORD_FLAGS : SEARCH_TERM_FLAGS).filter((name) => isFlagSet(flags, name));
|
|
98
|
+
if (foreign.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
const applicable = [...SHARED_FLAGS, ...(searchTerm ? SEARCH_TERM_FLAGS : TARGETING_KEYWORD_FLAGS)];
|
|
101
|
+
throw new Error(`${foreign.map((name) => dashed(name)).join(', ')} ${foreign.length === 1 ? 'has' : 'have'} no place in the params of an ` +
|
|
102
|
+
`operate_with "${searchTerm ? SEARCH_TERM : TARGETING_KEYWORD}" rule, and the API drops params it cannot ` +
|
|
103
|
+
`place. Applicable action flags: ${applicable.map((name) => dashed(name)).join(', ')}.`);
|
|
104
|
+
}
|
|
105
|
+
function missingFlagsError(missing) {
|
|
106
|
+
const lines = missing.map((name) => ` ${dashed(name)} — ${MISSING_REASONS[name]}`);
|
|
107
|
+
return new Error(`The ${ADD_AS_KEYWORD_ACTION} params are incomplete. Pass:\n${lines.join('\n')}`);
|
|
108
|
+
}
|
|
109
|
+
export function buildAddKeywordParams(operateWith, stored, flags) {
|
|
110
|
+
const searchTerm = isSearchTermRule(operateWith);
|
|
111
|
+
rejectForeignFlags(searchTerm, flags);
|
|
112
|
+
const params = asRecord(stored);
|
|
113
|
+
const missing = [];
|
|
114
|
+
const internalIds = flags['target-ad-group'] ?? storedInternalIds(params);
|
|
115
|
+
if (internalIds.length === 0)
|
|
116
|
+
missing.push('target-ad-group');
|
|
117
|
+
const matchType = flags['match-type'] ?? storedMatchType(params);
|
|
118
|
+
if (matchType === undefined)
|
|
119
|
+
missing.push('match-type');
|
|
120
|
+
const cptBidType = flags['cpt-bid-type'] ?? storedCptBidType(params);
|
|
121
|
+
if (cptBidType === undefined)
|
|
122
|
+
missing.push('cpt-bid-type');
|
|
123
|
+
const bid = flags['cpt-bid'];
|
|
124
|
+
if (bid !== undefined && cptBidType === BID_TYPE_IGNORING_VALUE) {
|
|
125
|
+
throw new Error(`--cpt-bid has no meaning with --cpt-bid-type ${BID_TYPE_IGNORING_VALUE}; that type copies the target ad group's default bid.`);
|
|
126
|
+
}
|
|
127
|
+
const cptBidValue = resolveCptBidValue(cptBidType, bid, params);
|
|
128
|
+
if (cptBidValue === undefined)
|
|
129
|
+
missing.push('cpt-bid');
|
|
130
|
+
if (missing.length > 0)
|
|
131
|
+
throw missingFlagsError(missing);
|
|
132
|
+
const built = {
|
|
133
|
+
cpt_bid: { type: cptBidType, value: cptBidValue ?? null },
|
|
134
|
+
match_type: matchType,
|
|
135
|
+
targets: { internal_ids: internalIds, type: 'ad-group' },
|
|
136
|
+
};
|
|
137
|
+
if (searchTerm) {
|
|
138
|
+
built.negate = resolveNegate(flags, params);
|
|
139
|
+
built.skip_enable_duplicate_keywords =
|
|
140
|
+
flags['skip-enable-duplicates'] ?? storedBoolean(params.skip_enable_duplicate_keywords) ?? false;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
built.pause_in_original_ad_group =
|
|
144
|
+
flags['pause-original'] ?? storedBoolean(params.pause_in_original_ad_group) ?? false;
|
|
145
|
+
}
|
|
146
|
+
return built;
|
|
147
|
+
}
|
|
148
|
+
export function requireSingleAction(actions) {
|
|
149
|
+
if (!Array.isArray(actions) || actions.length !== 1) {
|
|
150
|
+
throw new Error('A rule needs exactly one action: the API stores one action and one condition per rule.');
|
|
151
|
+
}
|
|
152
|
+
return asRecord(actions[0]);
|
|
153
|
+
}
|
|
154
|
+
export function rebuildAddKeywordAction(actions, operateWith, flags) {
|
|
155
|
+
const action = requireSingleAction(actions);
|
|
156
|
+
if (action.type !== ADD_AS_KEYWORD_ACTION) {
|
|
157
|
+
throw new Error(`The action flags only apply to an ${ADD_AS_KEYWORD_ACTION} action; this rule's action is "${String(action.type)}".`);
|
|
158
|
+
}
|
|
159
|
+
return [{ ...action, params: buildAddKeywordParams(operateWith, action.params, flags) }];
|
|
160
|
+
}
|
package/dist/lib/errors.js
CHANGED
|
@@ -2,7 +2,8 @@ export function describeListedError(error) {
|
|
|
2
2
|
const code = error.error_code ?? error.apple_error_code ?? error.validation_error_code ?? undefined;
|
|
3
3
|
const reason = error.message ?? error.apple_error_message ?? error.validation_error_message ?? code ?? 'rejected';
|
|
4
4
|
const position = error.input_ref === null || error.input_ref === undefined ? '' : ` (item ${error.input_ref + 1})`;
|
|
5
|
-
|
|
5
|
+
const field = error.field_name ? `${error.field_name}: ` : '';
|
|
6
|
+
return { code, text: `${field}${reason}${position}` };
|
|
6
7
|
}
|
|
7
8
|
export class ApiError extends Error {
|
|
8
9
|
statusCode;
|
package/oclif.manifest.json
CHANGED
|
@@ -2198,56 +2198,6 @@
|
|
|
2198
2198
|
"update.js"
|
|
2199
2199
|
]
|
|
2200
2200
|
},
|
|
2201
|
-
"asa:apps:list": {
|
|
2202
|
-
"aliases": [],
|
|
2203
|
-
"args": {},
|
|
2204
|
-
"description": "List the apps promoted by this company in Apple Search Ads",
|
|
2205
|
-
"examples": [
|
|
2206
|
-
"<%= config.bin %> asa apps list",
|
|
2207
|
-
"<%= config.bin %> asa apps list --page 2 --page-size 50"
|
|
2208
|
-
],
|
|
2209
|
-
"flags": {
|
|
2210
|
-
"json": {
|
|
2211
|
-
"description": "Format output as json.",
|
|
2212
|
-
"helpGroup": "GLOBAL",
|
|
2213
|
-
"name": "json",
|
|
2214
|
-
"allowNo": false,
|
|
2215
|
-
"type": "boolean"
|
|
2216
|
-
},
|
|
2217
|
-
"page": {
|
|
2218
|
-
"description": "Page number",
|
|
2219
|
-
"name": "page",
|
|
2220
|
-
"default": 1,
|
|
2221
|
-
"hasDynamicHelp": false,
|
|
2222
|
-
"multiple": false,
|
|
2223
|
-
"type": "option"
|
|
2224
|
-
},
|
|
2225
|
-
"page-size": {
|
|
2226
|
-
"description": "Items per page (max 1000); prefer one big page over a pagination loop",
|
|
2227
|
-
"name": "page-size",
|
|
2228
|
-
"default": 100,
|
|
2229
|
-
"hasDynamicHelp": false,
|
|
2230
|
-
"multiple": false,
|
|
2231
|
-
"type": "option"
|
|
2232
|
-
}
|
|
2233
|
-
},
|
|
2234
|
-
"hasDynamicHelp": false,
|
|
2235
|
-
"hiddenAliases": [],
|
|
2236
|
-
"id": "asa:apps:list",
|
|
2237
|
-
"pluginAlias": "adapty",
|
|
2238
|
-
"pluginName": "adapty",
|
|
2239
|
-
"pluginType": "core",
|
|
2240
|
-
"strict": true,
|
|
2241
|
-
"enableJsonFlag": true,
|
|
2242
|
-
"isESM": true,
|
|
2243
|
-
"relativePath": [
|
|
2244
|
-
"dist",
|
|
2245
|
-
"commands",
|
|
2246
|
-
"asa",
|
|
2247
|
-
"apps",
|
|
2248
|
-
"list.js"
|
|
2249
|
-
]
|
|
2250
|
-
},
|
|
2251
2201
|
"asa:ads:create": {
|
|
2252
2202
|
"aliases": [],
|
|
2253
2203
|
"args": {},
|
|
@@ -2529,13 +2479,65 @@
|
|
|
2529
2479
|
"update.js"
|
|
2530
2480
|
]
|
|
2531
2481
|
},
|
|
2482
|
+
"asa:apps:list": {
|
|
2483
|
+
"aliases": [],
|
|
2484
|
+
"args": {},
|
|
2485
|
+
"description": "List the apps promoted by this company in Apple Search Ads",
|
|
2486
|
+
"examples": [
|
|
2487
|
+
"<%= config.bin %> asa apps list",
|
|
2488
|
+
"<%= config.bin %> asa apps list --page 2 --page-size 50"
|
|
2489
|
+
],
|
|
2490
|
+
"flags": {
|
|
2491
|
+
"json": {
|
|
2492
|
+
"description": "Format output as json.",
|
|
2493
|
+
"helpGroup": "GLOBAL",
|
|
2494
|
+
"name": "json",
|
|
2495
|
+
"allowNo": false,
|
|
2496
|
+
"type": "boolean"
|
|
2497
|
+
},
|
|
2498
|
+
"page": {
|
|
2499
|
+
"description": "Page number",
|
|
2500
|
+
"name": "page",
|
|
2501
|
+
"default": 1,
|
|
2502
|
+
"hasDynamicHelp": false,
|
|
2503
|
+
"multiple": false,
|
|
2504
|
+
"type": "option"
|
|
2505
|
+
},
|
|
2506
|
+
"page-size": {
|
|
2507
|
+
"description": "Items per page (max 1000); prefer one big page over a pagination loop",
|
|
2508
|
+
"name": "page-size",
|
|
2509
|
+
"default": 100,
|
|
2510
|
+
"hasDynamicHelp": false,
|
|
2511
|
+
"multiple": false,
|
|
2512
|
+
"type": "option"
|
|
2513
|
+
}
|
|
2514
|
+
},
|
|
2515
|
+
"hasDynamicHelp": false,
|
|
2516
|
+
"hiddenAliases": [],
|
|
2517
|
+
"id": "asa:apps:list",
|
|
2518
|
+
"pluginAlias": "adapty",
|
|
2519
|
+
"pluginName": "adapty",
|
|
2520
|
+
"pluginType": "core",
|
|
2521
|
+
"strict": true,
|
|
2522
|
+
"enableJsonFlag": true,
|
|
2523
|
+
"isESM": true,
|
|
2524
|
+
"relativePath": [
|
|
2525
|
+
"dist",
|
|
2526
|
+
"commands",
|
|
2527
|
+
"asa",
|
|
2528
|
+
"apps",
|
|
2529
|
+
"list.js"
|
|
2530
|
+
]
|
|
2531
|
+
},
|
|
2532
2532
|
"asa:automations:create": {
|
|
2533
2533
|
"aliases": [],
|
|
2534
2534
|
"args": {},
|
|
2535
|
-
"description": "Create an automation rule from a JSON rule file",
|
|
2535
|
+
"description": "Create an automation rule from a JSON rule file. The add-as-keyword action flags fill in or override actions[0].params, so the bid, the match type and the target ad groups need no hand-written JSON.",
|
|
2536
2536
|
"examples": [
|
|
2537
2537
|
"<%= config.bin %> asa automations create --file rule.json",
|
|
2538
|
-
"<%= config.bin %> asa automations create --file rule.json --run-now"
|
|
2538
|
+
"<%= config.bin %> asa automations create --file rule.json --run-now",
|
|
2539
|
+
"<%= config.bin %> asa automations create --file rule.json --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type search_term_current_cpt",
|
|
2540
|
+
"<%= config.bin %> asa automations create --file rule.json --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type set_to --cpt-bid 1.50 --negate ad-group"
|
|
2539
2541
|
],
|
|
2540
2542
|
"flags": {
|
|
2541
2543
|
"json": {
|
|
@@ -2545,6 +2547,79 @@
|
|
|
2545
2547
|
"allowNo": false,
|
|
2546
2548
|
"type": "boolean"
|
|
2547
2549
|
},
|
|
2550
|
+
"cpt-bid": {
|
|
2551
|
+
"description": "cpt_bid.value: the bid itself with --cpt-bid-type set_to, or a percent markup on the entity bid with search_term_current_cpt / keyword_current_bid; not accepted with ad_group_default_bid",
|
|
2552
|
+
"name": "cpt-bid",
|
|
2553
|
+
"hasDynamicHelp": false,
|
|
2554
|
+
"multiple": false,
|
|
2555
|
+
"type": "option"
|
|
2556
|
+
},
|
|
2557
|
+
"cpt-bid-type": {
|
|
2558
|
+
"description": "Where the bid of the created keyword comes from; the API has no default",
|
|
2559
|
+
"name": "cpt-bid-type",
|
|
2560
|
+
"hasDynamicHelp": false,
|
|
2561
|
+
"multiple": false,
|
|
2562
|
+
"options": [
|
|
2563
|
+
"ad_group_default_bid",
|
|
2564
|
+
"keyword_current_bid",
|
|
2565
|
+
"search_term_current_cpt",
|
|
2566
|
+
"set_to"
|
|
2567
|
+
],
|
|
2568
|
+
"type": "option"
|
|
2569
|
+
},
|
|
2570
|
+
"match-type": {
|
|
2571
|
+
"description": "Match type of the created keyword; the API has no default",
|
|
2572
|
+
"name": "match-type",
|
|
2573
|
+
"hasDynamicHelp": false,
|
|
2574
|
+
"multiple": false,
|
|
2575
|
+
"options": [
|
|
2576
|
+
"BROAD",
|
|
2577
|
+
"EXACT"
|
|
2578
|
+
],
|
|
2579
|
+
"type": "option"
|
|
2580
|
+
},
|
|
2581
|
+
"negate": {
|
|
2582
|
+
"description": "Also add the search term as a negative keyword at this level (search-term rules)",
|
|
2583
|
+
"exclusive": [
|
|
2584
|
+
"no-negate"
|
|
2585
|
+
],
|
|
2586
|
+
"name": "negate",
|
|
2587
|
+
"hasDynamicHelp": false,
|
|
2588
|
+
"multiple": false,
|
|
2589
|
+
"options": [
|
|
2590
|
+
"ad-group",
|
|
2591
|
+
"campaign"
|
|
2592
|
+
],
|
|
2593
|
+
"type": "option"
|
|
2594
|
+
},
|
|
2595
|
+
"no-negate": {
|
|
2596
|
+
"description": "Leave no negative keyword behind (search-term rules)",
|
|
2597
|
+
"exclusive": [
|
|
2598
|
+
"negate"
|
|
2599
|
+
],
|
|
2600
|
+
"name": "no-negate",
|
|
2601
|
+
"allowNo": false,
|
|
2602
|
+
"type": "boolean"
|
|
2603
|
+
},
|
|
2604
|
+
"pause-original": {
|
|
2605
|
+
"description": "Pause the source keyword in its own ad group (targeting-keyword rules)",
|
|
2606
|
+
"name": "pause-original",
|
|
2607
|
+
"allowNo": true,
|
|
2608
|
+
"type": "boolean"
|
|
2609
|
+
},
|
|
2610
|
+
"skip-enable-duplicates": {
|
|
2611
|
+
"description": "Skip a keyword that already exists in the target ad group instead of enabling it (search-term rules)",
|
|
2612
|
+
"name": "skip-enable-duplicates",
|
|
2613
|
+
"allowNo": true,
|
|
2614
|
+
"type": "boolean"
|
|
2615
|
+
},
|
|
2616
|
+
"target-ad-group": {
|
|
2617
|
+
"description": "Ad group the keyword is added to (UUID), repeatable; a rule without one does nothing",
|
|
2618
|
+
"name": "target-ad-group",
|
|
2619
|
+
"hasDynamicHelp": false,
|
|
2620
|
+
"multiple": true,
|
|
2621
|
+
"type": "option"
|
|
2622
|
+
},
|
|
2548
2623
|
"yes": {
|
|
2549
2624
|
"char": "y",
|
|
2550
2625
|
"description": "Apply without asking; required when the output is piped or --json is used",
|
|
@@ -2803,10 +2878,11 @@
|
|
|
2803
2878
|
"required": true
|
|
2804
2879
|
}
|
|
2805
2880
|
},
|
|
2806
|
-
"description": "Change an automation rule: stop it, rename it, or replace parts of the rule",
|
|
2881
|
+
"description": "Change an automation rule: stop it, rename it, or replace parts of the rule. An add-as-keyword action flag turns the call into a read-modify-write: the rule is read, actions[0].params is rebuilt from the flags and the whole actions list is written back — an edit someone makes in the dashboard in between is overwritten. This is also the way to repair a rule whose stored params carry the wrong shape.",
|
|
2807
2882
|
"examples": [
|
|
2808
2883
|
"<%= config.bin %> asa automations update UUID --stop",
|
|
2809
|
-
"<%= config.bin %> asa automations update UUID --file rule.json"
|
|
2884
|
+
"<%= config.bin %> asa automations update UUID --file rule.json",
|
|
2885
|
+
"<%= config.bin %> asa automations update UUID --target-ad-group AD_GROUP_ID --match-type EXACT --cpt-bid-type search_term_current_cpt"
|
|
2810
2886
|
],
|
|
2811
2887
|
"flags": {
|
|
2812
2888
|
"json": {
|
|
@@ -2816,6 +2892,79 @@
|
|
|
2816
2892
|
"allowNo": false,
|
|
2817
2893
|
"type": "boolean"
|
|
2818
2894
|
},
|
|
2895
|
+
"cpt-bid": {
|
|
2896
|
+
"description": "cpt_bid.value: the bid itself with --cpt-bid-type set_to, or a percent markup on the entity bid with search_term_current_cpt / keyword_current_bid; not accepted with ad_group_default_bid",
|
|
2897
|
+
"name": "cpt-bid",
|
|
2898
|
+
"hasDynamicHelp": false,
|
|
2899
|
+
"multiple": false,
|
|
2900
|
+
"type": "option"
|
|
2901
|
+
},
|
|
2902
|
+
"cpt-bid-type": {
|
|
2903
|
+
"description": "Where the bid of the created keyword comes from; the API has no default",
|
|
2904
|
+
"name": "cpt-bid-type",
|
|
2905
|
+
"hasDynamicHelp": false,
|
|
2906
|
+
"multiple": false,
|
|
2907
|
+
"options": [
|
|
2908
|
+
"ad_group_default_bid",
|
|
2909
|
+
"keyword_current_bid",
|
|
2910
|
+
"search_term_current_cpt",
|
|
2911
|
+
"set_to"
|
|
2912
|
+
],
|
|
2913
|
+
"type": "option"
|
|
2914
|
+
},
|
|
2915
|
+
"match-type": {
|
|
2916
|
+
"description": "Match type of the created keyword; the API has no default",
|
|
2917
|
+
"name": "match-type",
|
|
2918
|
+
"hasDynamicHelp": false,
|
|
2919
|
+
"multiple": false,
|
|
2920
|
+
"options": [
|
|
2921
|
+
"BROAD",
|
|
2922
|
+
"EXACT"
|
|
2923
|
+
],
|
|
2924
|
+
"type": "option"
|
|
2925
|
+
},
|
|
2926
|
+
"negate": {
|
|
2927
|
+
"description": "Also add the search term as a negative keyword at this level (search-term rules)",
|
|
2928
|
+
"exclusive": [
|
|
2929
|
+
"no-negate"
|
|
2930
|
+
],
|
|
2931
|
+
"name": "negate",
|
|
2932
|
+
"hasDynamicHelp": false,
|
|
2933
|
+
"multiple": false,
|
|
2934
|
+
"options": [
|
|
2935
|
+
"ad-group",
|
|
2936
|
+
"campaign"
|
|
2937
|
+
],
|
|
2938
|
+
"type": "option"
|
|
2939
|
+
},
|
|
2940
|
+
"no-negate": {
|
|
2941
|
+
"description": "Leave no negative keyword behind (search-term rules)",
|
|
2942
|
+
"exclusive": [
|
|
2943
|
+
"negate"
|
|
2944
|
+
],
|
|
2945
|
+
"name": "no-negate",
|
|
2946
|
+
"allowNo": false,
|
|
2947
|
+
"type": "boolean"
|
|
2948
|
+
},
|
|
2949
|
+
"pause-original": {
|
|
2950
|
+
"description": "Pause the source keyword in its own ad group (targeting-keyword rules)",
|
|
2951
|
+
"name": "pause-original",
|
|
2952
|
+
"allowNo": true,
|
|
2953
|
+
"type": "boolean"
|
|
2954
|
+
},
|
|
2955
|
+
"skip-enable-duplicates": {
|
|
2956
|
+
"description": "Skip a keyword that already exists in the target ad group instead of enabling it (search-term rules)",
|
|
2957
|
+
"name": "skip-enable-duplicates",
|
|
2958
|
+
"allowNo": true,
|
|
2959
|
+
"type": "boolean"
|
|
2960
|
+
},
|
|
2961
|
+
"target-ad-group": {
|
|
2962
|
+
"description": "Ad group the keyword is added to (UUID), repeatable; a rule without one does nothing",
|
|
2963
|
+
"name": "target-ad-group",
|
|
2964
|
+
"hasDynamicHelp": false,
|
|
2965
|
+
"multiple": true,
|
|
2966
|
+
"type": "option"
|
|
2967
|
+
},
|
|
2819
2968
|
"yes": {
|
|
2820
2969
|
"char": "y",
|
|
2821
2970
|
"description": "Apply without asking; required when the output is piped or --json is used",
|
|
@@ -2880,47 +3029,6 @@
|
|
|
2880
3029
|
"update.js"
|
|
2881
3030
|
]
|
|
2882
3031
|
},
|
|
2883
|
-
"asa:competitors:summary": {
|
|
2884
|
-
"aliases": [],
|
|
2885
|
-
"args": {},
|
|
2886
|
-
"description": "Competitor summary for up to five App Store apps; the server covers the last full month and every country",
|
|
2887
|
-
"examples": [
|
|
2888
|
-
"<%= config.bin %> asa competitors summary --app-ids 1668337467,6503873027"
|
|
2889
|
-
],
|
|
2890
|
-
"flags": {
|
|
2891
|
-
"json": {
|
|
2892
|
-
"description": "Format output as json.",
|
|
2893
|
-
"helpGroup": "GLOBAL",
|
|
2894
|
-
"name": "json",
|
|
2895
|
-
"allowNo": false,
|
|
2896
|
-
"type": "boolean"
|
|
2897
|
-
},
|
|
2898
|
-
"app-ids": {
|
|
2899
|
-
"description": "Apple App Store IDs (adam_id), comma-separated, 1-5 values",
|
|
2900
|
-
"name": "app-ids",
|
|
2901
|
-
"required": true,
|
|
2902
|
-
"hasDynamicHelp": false,
|
|
2903
|
-
"multiple": false,
|
|
2904
|
-
"type": "option"
|
|
2905
|
-
}
|
|
2906
|
-
},
|
|
2907
|
-
"hasDynamicHelp": false,
|
|
2908
|
-
"hiddenAliases": [],
|
|
2909
|
-
"id": "asa:competitors:summary",
|
|
2910
|
-
"pluginAlias": "adapty",
|
|
2911
|
-
"pluginName": "adapty",
|
|
2912
|
-
"pluginType": "core",
|
|
2913
|
-
"strict": true,
|
|
2914
|
-
"enableJsonFlag": true,
|
|
2915
|
-
"isESM": true,
|
|
2916
|
-
"relativePath": [
|
|
2917
|
-
"dist",
|
|
2918
|
-
"commands",
|
|
2919
|
-
"asa",
|
|
2920
|
-
"competitors",
|
|
2921
|
-
"summary.js"
|
|
2922
|
-
]
|
|
2923
|
-
},
|
|
2924
3032
|
"asa:campaigns:bulk-create": {
|
|
2925
3033
|
"aliases": [],
|
|
2926
3034
|
"args": {},
|
|
@@ -3639,6 +3747,47 @@
|
|
|
3639
3747
|
"update.js"
|
|
3640
3748
|
]
|
|
3641
3749
|
},
|
|
3750
|
+
"asa:competitors:summary": {
|
|
3751
|
+
"aliases": [],
|
|
3752
|
+
"args": {},
|
|
3753
|
+
"description": "Competitor summary for up to five App Store apps; the server covers the last full month and every country",
|
|
3754
|
+
"examples": [
|
|
3755
|
+
"<%= config.bin %> asa competitors summary --app-ids 1668337467,6503873027"
|
|
3756
|
+
],
|
|
3757
|
+
"flags": {
|
|
3758
|
+
"json": {
|
|
3759
|
+
"description": "Format output as json.",
|
|
3760
|
+
"helpGroup": "GLOBAL",
|
|
3761
|
+
"name": "json",
|
|
3762
|
+
"allowNo": false,
|
|
3763
|
+
"type": "boolean"
|
|
3764
|
+
},
|
|
3765
|
+
"app-ids": {
|
|
3766
|
+
"description": "Apple App Store IDs (adam_id), comma-separated, 1-5 values",
|
|
3767
|
+
"name": "app-ids",
|
|
3768
|
+
"required": true,
|
|
3769
|
+
"hasDynamicHelp": false,
|
|
3770
|
+
"multiple": false,
|
|
3771
|
+
"type": "option"
|
|
3772
|
+
}
|
|
3773
|
+
},
|
|
3774
|
+
"hasDynamicHelp": false,
|
|
3775
|
+
"hiddenAliases": [],
|
|
3776
|
+
"id": "asa:competitors:summary",
|
|
3777
|
+
"pluginAlias": "adapty",
|
|
3778
|
+
"pluginName": "adapty",
|
|
3779
|
+
"pluginType": "core",
|
|
3780
|
+
"strict": true,
|
|
3781
|
+
"enableJsonFlag": true,
|
|
3782
|
+
"isESM": true,
|
|
3783
|
+
"relativePath": [
|
|
3784
|
+
"dist",
|
|
3785
|
+
"commands",
|
|
3786
|
+
"asa",
|
|
3787
|
+
"competitors",
|
|
3788
|
+
"summary.js"
|
|
3789
|
+
]
|
|
3790
|
+
},
|
|
3642
3791
|
"asa:creatives:list": {
|
|
3643
3792
|
"aliases": [],
|
|
3644
3793
|
"args": {},
|
|
@@ -4009,13 +4158,14 @@
|
|
|
4009
4158
|
"asa:metrics": {
|
|
4010
4159
|
"aliases": [],
|
|
4011
4160
|
"args": {},
|
|
4012
|
-
"description": "Query metrics for any level of the account over a date range\n\nOne row per entity, already aggregated server-side and sorted by --order-by, so a top-N question is one\ncall with --order-by and --page-size N — never sum pages yourself. Account-level totals are one call to\nasa metrics overview instead. The date window is capped by the finest --group-by period: 28 days when\nday is grouped, 90 with no period grouping, 180 by week, 365 by month and coarser — widen the window by\ncoarsening the grouping, not by splitting into more calls. Each page is also capped at
|
|
4161
|
+
"description": "Query metrics for any level of the account over a date range\n\nOne row per entity, already aggregated server-side and sorted by --order-by, so a top-N question is one\ncall with --order-by and --page-size N — never sum pages yourself. Account-level totals are one call to\nasa metrics overview instead. The date window is capped by the finest --group-by period: 28 days when\nday is grouped, 90 with no period grouping, 180 by week, 365 by month and coarser — widen the window by\ncoarsening the grouping, not by splitting into more calls. Each page is also capped at 5000 breakdown\nrows (entities × countries × periods); over it the call fails with 422 cli_response_too_large — coarsen\nthe grouping, narrow the window, or reduce page[size]. Budget: 5 metrics calls per minute, at most\n2 per 10 seconds, one at a time.\n\n--metric is required and every metric named is computed over the whole entity set, so ask for the\ncolumns you actually read. subscribers and paid_subscribers (and arppu / arpas, which derive from them)\ncount unique profiles per entity and cost roughly seventeen times the rest; whatever the --entity, they\nare refused unless --campaign or --ad-group scopes the call. Those scope flags are also the cheapest way\nto make any call fast, since cost follows the number of entities aggregated, not the page size.",
|
|
4013
4162
|
"examples": [
|
|
4014
|
-
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31",
|
|
4015
|
-
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --order-by spend --page-size 5",
|
|
4016
|
-
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --group-by country --page-size 1000",
|
|
4163
|
+
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric adapty_installs",
|
|
4164
|
+
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --order-by spend --page-size 5",
|
|
4165
|
+
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --group-by country --page-size 1000",
|
|
4017
4166
|
"<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric spend --metric roas",
|
|
4018
|
-
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90"
|
|
4167
|
+
"<%= config.bin %> asa metrics --entity campaign --date-from 2026-07-01 --date-to 2026-07-31 --metric roas --by-days 7 --by-days 90",
|
|
4168
|
+
"<%= config.bin %> asa metrics --entity keyword --date-from 2026-07-01 --date-to 2026-07-31 --metric arpas --campaign 0f0e..."
|
|
4019
4169
|
],
|
|
4020
4170
|
"flags": {
|
|
4021
4171
|
"json": {
|
|
@@ -4048,6 +4198,27 @@
|
|
|
4048
4198
|
"multiple": true,
|
|
4049
4199
|
"type": "option"
|
|
4050
4200
|
},
|
|
4201
|
+
"ad-group": {
|
|
4202
|
+
"description": "Keep only rows in this ad group; repeatable",
|
|
4203
|
+
"name": "ad-group",
|
|
4204
|
+
"hasDynamicHelp": false,
|
|
4205
|
+
"multiple": true,
|
|
4206
|
+
"type": "option"
|
|
4207
|
+
},
|
|
4208
|
+
"app": {
|
|
4209
|
+
"description": "Keep only rows in this app; repeatable",
|
|
4210
|
+
"name": "app",
|
|
4211
|
+
"hasDynamicHelp": false,
|
|
4212
|
+
"multiple": true,
|
|
4213
|
+
"type": "option"
|
|
4214
|
+
},
|
|
4215
|
+
"campaign": {
|
|
4216
|
+
"description": "Keep only rows in this campaign; repeatable",
|
|
4217
|
+
"name": "campaign",
|
|
4218
|
+
"hasDynamicHelp": false,
|
|
4219
|
+
"multiple": true,
|
|
4220
|
+
"type": "option"
|
|
4221
|
+
},
|
|
4051
4222
|
"date-from": {
|
|
4052
4223
|
"description": "Start of the period (YYYY-MM-DD)",
|
|
4053
4224
|
"name": "date-from",
|
|
@@ -4094,8 +4265,9 @@
|
|
|
4094
4265
|
"type": "option"
|
|
4095
4266
|
},
|
|
4096
4267
|
"metric": {
|
|
4097
|
-
"description": "Metric name (dashboard nomenclature, e.g. spend, taps, gross_roas), repeatable;
|
|
4268
|
+
"description": "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",
|
|
4098
4269
|
"name": "metric",
|
|
4270
|
+
"required": true,
|
|
4099
4271
|
"hasDynamicHelp": false,
|
|
4100
4272
|
"multiple": true,
|
|
4101
4273
|
"type": "option"
|
|
@@ -5009,5 +5181,5 @@
|
|
|
5009
5181
|
]
|
|
5010
5182
|
}
|
|
5011
5183
|
},
|
|
5012
|
-
"version": "0.8.4
|
|
5184
|
+
"version": "0.8.4"
|
|
5013
5185
|
}
|