adapty 0.9.4 → 0.10.0
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 +55 -0
- package/bin/dev.js +6 -0
- package/bin/run.js +6 -0
- package/dist/cli/commands/feedback.d.ts +16 -0
- package/dist/cli/commands/feedback.js +36 -0
- package/dist/cli/input/segment.d.ts +20 -0
- package/dist/cli/input/segment.js +61 -0
- package/dist/commands/asa/segments/create.d.ts +17 -0
- package/dist/commands/asa/segments/create.js +53 -0
- package/dist/commands/asa/segments/list.d.ts +16 -0
- package/dist/commands/asa/segments/list.js +59 -0
- package/dist/commands/asa/segments/update.d.ts +20 -0
- package/dist/commands/asa/segments/update.js +60 -0
- package/dist/commands/feedback.d.ts +1 -0
- package/dist/commands/feedback.js +1 -0
- package/dist/commands/segments/create.d.ts +14 -0
- package/dist/commands/segments/create.js +30 -0
- package/dist/commands/segments/update.d.ts +17 -0
- package/dist/commands/segments/update.js +35 -0
- package/dist/lib/api-schemas.d.ts +24 -0
- package/dist/lib/asa-client.d.ts +3 -0
- package/dist/lib/asa-client.js +26 -0
- package/dist/lib/asa-flags.d.ts +31 -0
- package/dist/lib/asa-flags.js +69 -0
- package/dist/sdk/adapty/feedback/index.d.ts +4 -0
- package/dist/sdk/adapty/feedback/index.js +2 -0
- package/dist/sdk/adapty/feedback/resource.d.ts +6 -0
- package/dist/sdk/adapty/feedback/resource.js +8 -0
- package/dist/sdk/adapty/feedback/send.d.ts +18 -0
- package/dist/sdk/adapty/feedback/send.js +36 -0
- package/dist/sdk/adapty/index.d.ts +4 -0
- package/dist/sdk/adapty/index.js +3 -0
- package/oclif.manifest.json +2018 -1588
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,24 @@ adapty placements create --app UUID [flags]
|
|
|
90
90
|
adapty placements update --app UUID PLACEMENT_ID [flags]
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Segments
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
adapty segments list --app UUID [--page N] [--page-size N]
|
|
97
|
+
adapty segments get --app UUID SEGMENT_ID # with filters, profile_count, refresh_in_progress
|
|
98
|
+
adapty segments create --app UUID --title "US installs" --filter ip_country:IN:US --filter install_date:>=:2026-01-01
|
|
99
|
+
adapty segments update --app UUID SEGMENT_ID --title "..." --filter campaign:IN:2144520245
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A filter is `field:OPERATOR:value[,value...]`, or `field:IS NULL` / `field:IS NOT NULL` with no value. Operators:
|
|
103
|
+
`IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, `>`, `>=`, `<`, `<=`, `BETWEEN`. Numeric fields (`age`,
|
|
104
|
+
`total_revenue_usd`, `@float` custom attributes) take the plain number — `age:>:18` — the server converts it. For
|
|
105
|
+
anything the short form cannot express pass one JSON object: `--filter '{"field_name":"age","operator":">","value_list":[18]}'`.
|
|
106
|
+
The server validates field names, operators and value types and reports one error per field.
|
|
107
|
+
`update` replaces the whole segment — pass every filter you want to keep. Segments
|
|
108
|
+
built from Apple Search Ads entities are easier to create with `adapty asa segments create`, which resolves the
|
|
109
|
+
Apple ids and names the segment the way the dashboard does (see [Apple Search Ads](#apple-search-ads)).
|
|
110
|
+
|
|
93
111
|
### Flows
|
|
94
112
|
|
|
95
113
|
```sh
|
|
@@ -275,6 +293,21 @@ When several agents are found, the command asks which to install into; with no o
|
|
|
275
293
|
`npx skills add adaptyteam/adapty-skills --all --global`, which covers the other agents. Restart an agent to
|
|
276
294
|
load the skills. Exit **1** means an install failed.
|
|
277
295
|
|
|
296
|
+
### Feedback
|
|
297
|
+
|
|
298
|
+
A short report to Adapty on how a task with the CLI or an Adapty skill went. Leave out secrets and personal
|
|
299
|
+
data. There is no `--app`: put app, flow or other ids into the message when they matter:
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
adapty feedback --sentiment positive --message "Imported the paywall from Canva in one pass"
|
|
303
|
+
adapty feedback --sentiment negative --message "flows config update rejected a valid button twice" \
|
|
304
|
+
--feature flow-generator --context "editing the paywall screen"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`--sentiment` is `positive`, `neutral` or `negative`. `--feature` takes letters, digits, `.`, `_` and `-`;
|
|
308
|
+
Adapty records `unknown` when it is left out. The CLI won't send a report containing something that looks like
|
|
309
|
+
an API key, a bearer token, a JWT or an email address; it exits 2 and names the flag to fix.
|
|
310
|
+
|
|
278
311
|
### Apple Search Ads
|
|
279
312
|
|
|
280
313
|
Apple Search Ads commands live under `adapty asa` and talk to the ASA service rather than the Developer
|
|
@@ -299,6 +332,28 @@ adapty asa keywords list --ad-group AD_GROUP_UUID --ad-group OTHER_UUID # repe
|
|
|
299
332
|
adapty asa creatives list --app APP_UUID
|
|
300
333
|
```
|
|
301
334
|
|
|
335
|
+
#### Segments from Apple Search Ads entities
|
|
336
|
+
|
|
337
|
+
The Ads Manager "create segment" button, as commands. Each one builds an Adapty segment with a single
|
|
338
|
+
attribution filter over the Apple ids of the chosen entities and names it `[ASA] Campaign: <name>` (or the ad
|
|
339
|
+
group / keyword wording), so the dashboard recognises it as its own. These commands write through the Developer
|
|
340
|
+
API, which is why they are the one place in the `asa` topic that takes `--app`: the Adapty app the segment
|
|
341
|
+
belongs to, from `adapty apps list`.
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
adapty asa segments create --app APP_UUID --campaign CAMPAIGN_UUID
|
|
345
|
+
adapty asa segments create --app APP_UUID --campaign UUID_1 --campaign UUID_2 --title "[ASA] Brand campaigns"
|
|
346
|
+
adapty asa segments create --app APP_UUID --ad-group AD_GROUP_UUID
|
|
347
|
+
adapty asa segments create --app APP_UUID --ad-group AD_GROUP_UUID --keyword KEYWORD_UUID --keyword OTHER_UUID
|
|
348
|
+
adapty asa segments update SEGMENT_UUID --app APP_UUID --campaign CAMPAIGN_UUID # keeps title and description
|
|
349
|
+
adapty asa segments list --app APP_UUID [--campaign CAMPAIGN_UUID | --ad-group AD_GROUP_UUID]
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
One kind of entity per call. `--keyword` needs exactly one `--ad-group` to look the keywords up in. `list` shows
|
|
353
|
+
the `[ASA]`-titled segments; with `--campaign` or `--ad-group` it keeps only those whose filter carries that
|
|
354
|
+
entity, so a script can check for an existing segment before creating one. Any other filter shape goes through
|
|
355
|
+
`adapty segments create`.
|
|
356
|
+
|
|
302
357
|
`--campaign-group`, `--app`, `--campaign`, `--ad-group` are repeatable and take the UUIDs printed by the
|
|
303
358
|
matching list command; `--search` matches names case-insensitively. Each list accepts only the filters that
|
|
304
359
|
make sense for it: `--ad-group` starts at keywords, negative keywords, search terms and ads, `--status` is
|
package/bin/dev.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
|
|
2
2
|
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
|
|
3
5
|
import { execute } from '@oclif/core';
|
|
4
6
|
|
|
7
|
+
// Node abandons every address but the last after 250ms. On a slow link the working address is
|
|
8
|
+
// dropped and the last one, e.g. IPv6 with no route, fails. Never lower a user-set value.
|
|
9
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(Math.max(net.getDefaultAutoSelectFamilyAttemptTimeout(), 1000));
|
|
10
|
+
|
|
5
11
|
await execute({ development: true, dir: import.meta.url });
|
package/bin/run.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
|
|
3
5
|
import { execute } from '@oclif/core';
|
|
4
6
|
|
|
7
|
+
// Node abandons every address but the last after 250ms. On a slow link the working address is
|
|
8
|
+
// dropped and the last one, e.g. IPv6 with no route, fails. Never lower a user-set value.
|
|
9
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(Math.max(net.getDefaultAutoSelectFamilyAttemptTimeout(), 1000));
|
|
10
|
+
|
|
5
11
|
await execute({ dir: import.meta.url });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AdaptyCommand } from '../base/adapty/index.js';
|
|
2
|
+
type Result = {
|
|
3
|
+
status: 'sent';
|
|
4
|
+
};
|
|
5
|
+
export default class Feedback extends AdaptyCommand {
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
sentiment: import("@oclif/core/interfaces").OptionFlag<"positive" | "neutral" | "negative", import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
message: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
feature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<Result>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { validateSendFeedback } from '../../sdk/adapty/feedback/index.js';
|
|
3
|
+
import { sentiments } from '../../sdk/adapty/index.js';
|
|
4
|
+
import { assertValid } from '../../sdk/core/validation.js';
|
|
5
|
+
import { AdaptyCommand } from '../base/adapty/index.js';
|
|
6
|
+
export default class Feedback extends AdaptyCommand {
|
|
7
|
+
static description = [
|
|
8
|
+
'Send a short report on how a task with the Adapty CLI or an Adapty skill went.',
|
|
9
|
+
'Meant for AI agents: say what worked, what failed or confused you. Put app, flow and other ids into the message if they matter.',
|
|
10
|
+
'Leave out secrets and personal data.',
|
|
11
|
+
].join('\n');
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> feedback --sentiment negative --feature flow-generator --message "flows config update rejected a valid button component twice"',
|
|
14
|
+
'<%= config.bin %> feedback --sentiment positive --message "Imported the paywall from Canva in one pass" --context "Building an onboarding flow from a Canva file"',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
sentiment: Flags.option({ description: 'How the task went', options: sentiments, required: true })(),
|
|
18
|
+
message: Flags.string({ description: 'The report: a few sentences on what happened', required: true }),
|
|
19
|
+
feature: Flags.string({ description: 'Feature or skill the report comes from; Adapty records "unknown" when omitted' }),
|
|
20
|
+
context: Flags.string({ description: 'What you were doing at that moment' }),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { flags } = await this.parse(Feedback);
|
|
24
|
+
const input = {
|
|
25
|
+
context: flags.context,
|
|
26
|
+
feature: flags.feature,
|
|
27
|
+
message: flags.message,
|
|
28
|
+
sentiment: flags.sentiment,
|
|
29
|
+
};
|
|
30
|
+
assertValid(validateSendFeedback(input));
|
|
31
|
+
await this.adapty.feedback.send(input);
|
|
32
|
+
const result = { status: 'sent' };
|
|
33
|
+
this.render(result, () => 'Feedback sent. Thank you!');
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One filter of a segment body, as the developer API takes it. */
|
|
2
|
+
export type SegmentFilterInput = {
|
|
3
|
+
field_name: string;
|
|
4
|
+
operator: string;
|
|
5
|
+
value_list: unknown[];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Values stay strings on the short form: the server turns them into numbers for numeric fields. The JSON form is
|
|
9
|
+
* for typed or nested values the short form cannot express.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseSegmentFilter(input: string): SegmentFilterInput;
|
|
12
|
+
/** Spread into a write command: `static flags = { ...appIdFlag, ...segmentWriteFlags }`. */
|
|
13
|
+
export declare const segmentWriteFlags: {
|
|
14
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
filter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
export declare function segmentFilters(flags: {
|
|
19
|
+
filter?: string[] | undefined;
|
|
20
|
+
}): SegmentFilterInput[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { usageError } from '../errors.js';
|
|
3
|
+
const FILTER_USAGE = 'Filters are written as field:OPERATOR:value[,value...] (e.g. campaign:IN:2144520245), as field:IS NULL, '
|
|
4
|
+
+ 'or as one JSON object {"field_name": ..., "operator": ..., "value_list": [...]}.';
|
|
5
|
+
const EXISTENCE_OPERATORS = new Set(['IS NOT NULL', 'IS NULL']);
|
|
6
|
+
function parseJsonSegmentFilter(raw) {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw usageError(`The filter is not valid JSON. ${FILTER_USAGE}`);
|
|
13
|
+
}
|
|
14
|
+
const { field_name: fieldName, operator, value_list: valueList } = (parsed ?? {});
|
|
15
|
+
if (typeof fieldName !== 'string' || typeof operator !== 'string' || !Array.isArray(valueList)) {
|
|
16
|
+
throw usageError(`A JSON filter needs field_name, operator and value_list. ${FILTER_USAGE}`);
|
|
17
|
+
}
|
|
18
|
+
return { field_name: fieldName, operator, value_list: valueList };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Values stay strings on the short form: the server turns them into numbers for numeric fields. The JSON form is
|
|
22
|
+
* for typed or nested values the short form cannot express.
|
|
23
|
+
*/
|
|
24
|
+
export function parseSegmentFilter(input) {
|
|
25
|
+
const trimmed = input.trim();
|
|
26
|
+
if (trimmed.startsWith('{')) {
|
|
27
|
+
return parseJsonSegmentFilter(trimmed);
|
|
28
|
+
}
|
|
29
|
+
const [fieldName, rawOperator, ...rest] = trimmed.split(':');
|
|
30
|
+
const operator = rawOperator?.trim() ?? '';
|
|
31
|
+
if (!fieldName || !operator) {
|
|
32
|
+
throw usageError(FILTER_USAGE);
|
|
33
|
+
}
|
|
34
|
+
const valueList = rest.join(':').split(',').map(value => value.trim()).filter(value => value !== '');
|
|
35
|
+
if (EXISTENCE_OPERATORS.has(operator)) {
|
|
36
|
+
if (valueList.length > 0) {
|
|
37
|
+
throw usageError(`${operator} takes no values. ${FILTER_USAGE}`);
|
|
38
|
+
}
|
|
39
|
+
return { field_name: fieldName, operator, value_list: [] };
|
|
40
|
+
}
|
|
41
|
+
if (valueList.length === 0) {
|
|
42
|
+
throw usageError(`A filter needs at least one value. ${FILTER_USAGE}`);
|
|
43
|
+
}
|
|
44
|
+
return { field_name: fieldName, operator, value_list: valueList };
|
|
45
|
+
}
|
|
46
|
+
/** Spread into a write command: `static flags = { ...appIdFlag, ...segmentWriteFlags }`. */
|
|
47
|
+
export const segmentWriteFlags = {
|
|
48
|
+
description: Flags.string({ description: 'Segment description' }),
|
|
49
|
+
filter: Flags.string({
|
|
50
|
+
description: 'Filter as field:OPERATOR:value[,value...] (e.g. campaign:IN:2144520245), field:IS NULL, or a JSON '
|
|
51
|
+
+ 'object {"field_name","operator","value_list"}; repeatable',
|
|
52
|
+
multiple: true,
|
|
53
|
+
// oclif keeps a string flag a string: the parsed filter travels as JSON and segmentFilters unpacks it
|
|
54
|
+
parse: (input) => Promise.resolve(JSON.stringify(parseSegmentFilter(input))),
|
|
55
|
+
required: true,
|
|
56
|
+
}),
|
|
57
|
+
title: Flags.string({ description: 'Segment title', required: true }),
|
|
58
|
+
};
|
|
59
|
+
export function segmentFilters(flags) {
|
|
60
|
+
return (flags.filter ?? []).map(raw => JSON.parse(raw));
|
|
61
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { SegmentDetailDTO } from '../../../lib/api-schemas.js';
|
|
3
|
+
export default class AsaSegmentsCreate extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
keyword: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<SegmentDetailDTO>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { appIdFlag } from '../../../cli/input/app.js';
|
|
3
|
+
import { createAsaClient, fetchAsaSegmentEntities } from '../../../lib/asa-client.js';
|
|
4
|
+
import { ASA_SEGMENT_FIELD, asaSegmentSourceFlags, buildAsaSegmentNaming, resolveAsaSegmentSource } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
|
|
6
|
+
import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
|
|
7
|
+
import { printResponse } from '../../../lib/output.js';
|
|
8
|
+
export default class AsaSegmentsCreate extends Command {
|
|
9
|
+
static description = `Create an Adapty segment from Apple Search Ads campaigns, ad groups or keywords
|
|
10
|
+
|
|
11
|
+
Builds the same segment the Ads Manager "create segment" button does: one attribution filter over the Apple ids
|
|
12
|
+
of the chosen entities, named "[ASA] Campaign: <name>" unless --title is passed. Pass one kind of entity per call;
|
|
13
|
+
--keyword needs exactly one --ad-group to look the keywords up in. --app is the Adapty app the segment belongs to
|
|
14
|
+
(see \`adapty apps list\`).`;
|
|
15
|
+
static enableJsonFlag = true;
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> asa segments create --app APP_UUID --campaign CAMPAIGN_UUID',
|
|
18
|
+
'<%= config.bin %> asa segments create --app APP_UUID --campaign UUID_1 --campaign UUID_2 --title "[ASA] Brand campaigns"',
|
|
19
|
+
'<%= config.bin %> asa segments create --app APP_UUID --ad-group AD_GROUP_UUID --keyword KEYWORD_UUID',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...appIdFlag,
|
|
23
|
+
...asaSegmentSourceFlags,
|
|
24
|
+
...confirmFlags,
|
|
25
|
+
description: Flags.string({ description: 'Segment description; defaults to the dashboard wording' }),
|
|
26
|
+
title: Flags.string({ description: 'Segment title; defaults to the dashboard wording, e.g. "[ASA] Campaign: <name>"' }),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { flags } = await this.parse(AsaSegmentsCreate);
|
|
30
|
+
let selection;
|
|
31
|
+
try {
|
|
32
|
+
selection = resolveAsaSegmentSource(flags);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
this.error(error.message, { exit: 2 });
|
|
36
|
+
}
|
|
37
|
+
const asaClient = await createAsaClient(this);
|
|
38
|
+
const entities = await fetchAsaSegmentEntities(asaClient, selection);
|
|
39
|
+
const naming = buildAsaSegmentNaming(selection.source, entities);
|
|
40
|
+
const body = {
|
|
41
|
+
description: flags.description ?? naming.description,
|
|
42
|
+
filters: [{ field_name: ASA_SEGMENT_FIELD[selection.source], operator: 'IN', value_list: entities.map(entity => entity.id) }],
|
|
43
|
+
title: flags.title ?? naming.title,
|
|
44
|
+
};
|
|
45
|
+
const path = `/apps/${flags.app}/segments`;
|
|
46
|
+
await confirmMutation(this, { body, method: 'POST', path: `${path}/`, summary: `Create segment ${body.title}` }, flags.yes);
|
|
47
|
+
const client = await createAuthenticatedClient(this.config);
|
|
48
|
+
const result = await client.post(path, body);
|
|
49
|
+
this.log('Segment created!');
|
|
50
|
+
printResponse(result, this.log.bind(this));
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { SegmentDTO } from '../../../lib/api-schemas.js';
|
|
3
|
+
export default class AsaSegmentsList extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<SegmentDTO[]>;
|
|
13
|
+
/** The Apple id the segments must filter on, resolved from the ASA entity the user named. */
|
|
14
|
+
private wantedFilterValue;
|
|
15
|
+
private keepMatching;
|
|
16
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { appIdFlag } from '../../../cli/input/app.js';
|
|
3
|
+
import { createAsaClient, fetchAsaSegmentEntities } from '../../../lib/asa-client.js';
|
|
4
|
+
import { ASA_SEGMENT_FIELD, resolveAsaSegmentSource } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
|
|
6
|
+
import { isValidUuid } from '../../../lib/flags.js';
|
|
7
|
+
import { printList } from '../../../lib/output.js';
|
|
8
|
+
const ASA_TITLE_PREFIX = '[ASA]';
|
|
9
|
+
const parseId = (input) => (isValidUuid(input)
|
|
10
|
+
? Promise.resolve(input)
|
|
11
|
+
: Promise.reject(new Error('Ids are the UUIDs printed by the matching list command.')));
|
|
12
|
+
export default class AsaSegmentsList extends Command {
|
|
13
|
+
static description = `List the segments created from Apple Search Ads entities
|
|
14
|
+
|
|
15
|
+
Segments whose title starts with "[ASA]", the dashboard's own marker. With --campaign or --ad-group only the
|
|
16
|
+
segments whose filter carries that entity's Apple id are kept, so a script can tell whether a campaign already
|
|
17
|
+
has its segment before creating one.`;
|
|
18
|
+
static enableJsonFlag = true;
|
|
19
|
+
static examples = [
|
|
20
|
+
'<%= config.bin %> asa segments list --app APP_UUID',
|
|
21
|
+
'<%= config.bin %> asa segments list --app APP_UUID --campaign CAMPAIGN_UUID',
|
|
22
|
+
];
|
|
23
|
+
static flags = {
|
|
24
|
+
...appIdFlag,
|
|
25
|
+
'ad-group': Flags.string({ description: 'Keep only segments built from this ad group (UUID)', parse: parseId }),
|
|
26
|
+
'campaign': Flags.string({ description: 'Keep only segments built from this campaign (UUID)', parse: parseId }),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { flags } = await this.parse(AsaSegmentsList);
|
|
30
|
+
const wanted = await this.wantedFilterValue(flags);
|
|
31
|
+
const client = await createAuthenticatedClient(this.config);
|
|
32
|
+
const page = await client.get(`/apps/${flags.app}/segments`, { 'page[size]': '100' });
|
|
33
|
+
const asaSegments = page.data.filter(segment => segment.title.startsWith(ASA_TITLE_PREFIX));
|
|
34
|
+
const result = wanted === undefined
|
|
35
|
+
? asaSegments
|
|
36
|
+
: await this.keepMatching(client, flags.app, asaSegments, wanted);
|
|
37
|
+
printList(result, this.log.bind(this));
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/** The Apple id the segments must filter on, resolved from the ASA entity the user named. */
|
|
41
|
+
async wantedFilterValue(flags) {
|
|
42
|
+
if (flags.campaign === undefined && flags['ad-group'] === undefined) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (flags.campaign !== undefined && flags['ad-group'] !== undefined) {
|
|
46
|
+
this.error('Pass either --campaign or --ad-group, not both.', { exit: 2 });
|
|
47
|
+
}
|
|
48
|
+
const selection = resolveAsaSegmentSource({
|
|
49
|
+
'ad-group': flags['ad-group'] === undefined ? undefined : [flags['ad-group']],
|
|
50
|
+
'campaign': flags.campaign === undefined ? undefined : [flags.campaign],
|
|
51
|
+
});
|
|
52
|
+
const [entity] = await fetchAsaSegmentEntities(await createAsaClient(this), selection);
|
|
53
|
+
return { field: ASA_SEGMENT_FIELD[selection.source], value: entity?.id };
|
|
54
|
+
}
|
|
55
|
+
async keepMatching(client, app, segments, wanted) {
|
|
56
|
+
const details = await Promise.all(segments.map(segment => client.get(`/apps/${app}/segments/${segment.id}`)));
|
|
57
|
+
return details.filter(detail => detail.filters.some(filter => filter.field_name === wanted.field && filter.value_list.map(String).includes(wanted.value ?? '')));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { SegmentDetailDTO } from '../../../lib/api-schemas.js';
|
|
3
|
+
export default class AsaSegmentsUpdate extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
segment_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static enableJsonFlag: boolean;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'ad-group': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
campaign: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
keyword: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
};
|
|
19
|
+
run(): Promise<SegmentDetailDTO>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { appIdFlag } from '../../../cli/input/app.js';
|
|
3
|
+
import { createAsaClient, fetchAsaSegmentEntities } from '../../../lib/asa-client.js';
|
|
4
|
+
import { ASA_SEGMENT_FIELD, asaSegmentSourceFlags, resolveAsaSegmentSource } from '../../../lib/asa-flags.js';
|
|
5
|
+
import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
|
|
6
|
+
import { confirmFlags, confirmMutation } from '../../../lib/confirm.js';
|
|
7
|
+
import { isValidUuid } from '../../../lib/flags.js';
|
|
8
|
+
import { printResponse } from '../../../lib/output.js';
|
|
9
|
+
export default class AsaSegmentsUpdate extends Command {
|
|
10
|
+
static args = {
|
|
11
|
+
segment_id: Args.string({ description: 'Segment ID (UUID)', required: true }),
|
|
12
|
+
};
|
|
13
|
+
static description = `Point an existing segment at other Apple Search Ads campaigns, ad groups or keywords
|
|
14
|
+
|
|
15
|
+
Replaces the segment's filter with one attribution filter over the chosen entities, as the dashboard does.
|
|
16
|
+
Title and description stay as stored unless --title / --description are passed.`;
|
|
17
|
+
static enableJsonFlag = true;
|
|
18
|
+
static examples = [
|
|
19
|
+
'<%= config.bin %> asa segments update SEGMENT_UUID --app APP_UUID --campaign CAMPAIGN_UUID --campaign OTHER_UUID',
|
|
20
|
+
'<%= config.bin %> asa segments update SEGMENT_UUID --app APP_UUID --ad-group AD_GROUP_UUID --keyword KEYWORD_UUID --title "Top keywords"',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
...appIdFlag,
|
|
24
|
+
...asaSegmentSourceFlags,
|
|
25
|
+
...confirmFlags,
|
|
26
|
+
description: Flags.string({ description: 'New description; omit to keep the stored one' }),
|
|
27
|
+
title: Flags.string({ description: 'New title; omit to keep the stored one' }),
|
|
28
|
+
};
|
|
29
|
+
async run() {
|
|
30
|
+
const { args, flags } = await this.parse(AsaSegmentsUpdate);
|
|
31
|
+
if (!isValidUuid(args.segment_id)) {
|
|
32
|
+
this.error('Invalid segment ID format.', { exit: 2 });
|
|
33
|
+
}
|
|
34
|
+
let selection;
|
|
35
|
+
try {
|
|
36
|
+
selection = resolveAsaSegmentSource(flags);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
this.error(error.message, { exit: 2 });
|
|
40
|
+
}
|
|
41
|
+
const asaClient = await createAsaClient(this);
|
|
42
|
+
const entities = await fetchAsaSegmentEntities(asaClient, selection);
|
|
43
|
+
const client = await createAuthenticatedClient(this.config);
|
|
44
|
+
const path = `/apps/${flags.app}/segments/${args.segment_id}`;
|
|
45
|
+
// PUT replaces the whole segment, so the stored title and description are read back unless overridden
|
|
46
|
+
const overridesBoth = flags.title !== undefined && flags.description !== undefined;
|
|
47
|
+
const stored = overridesBoth ? undefined : await client.get(path);
|
|
48
|
+
const body = {
|
|
49
|
+
description: flags.description ?? stored?.description ?? undefined,
|
|
50
|
+
filters: [{ field_name: ASA_SEGMENT_FIELD[selection.source], operator: 'IN', value_list: entities.map(entity => entity.id) }],
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- stored is read whenever title is missing
|
|
52
|
+
title: flags.title ?? stored.title,
|
|
53
|
+
};
|
|
54
|
+
await confirmMutation(this, { body, method: 'PUT', path: `${path}/`, summary: `Replace filters of segment ${args.segment_id}` }, flags.yes);
|
|
55
|
+
const result = await client.put(path, body);
|
|
56
|
+
this.log('Segment updated!');
|
|
57
|
+
printResponse(result, this.log.bind(this));
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '../cli/commands/feedback.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '../cli/commands/feedback.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { SegmentDetailDTO } from '../../lib/api-schemas.js';
|
|
3
|
+
export default class SegmentsCreate extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
filter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<SegmentDetailDTO>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { appIdFlag } from '../../cli/input/app.js';
|
|
3
|
+
import { segmentFilters, segmentWriteFlags } from '../../cli/input/segment.js';
|
|
4
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
5
|
+
import { printResponse } from '../../lib/output.js';
|
|
6
|
+
export default class SegmentsCreate extends Command {
|
|
7
|
+
static description = `Create a segment from filters over profile fields
|
|
8
|
+
|
|
9
|
+
Filters are written as field:OPERATOR:value[,value...] and validated by the server. Apple Search Ads
|
|
10
|
+
attribution uses campaign, ad_group and creative (a keyword) with Apple ids as values; build those from
|
|
11
|
+
ASA entities with \`asa segments create\` instead of copying ids by hand.`;
|
|
12
|
+
static enableJsonFlag = true;
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> segments create --app UUID --title "[ASA] Campaign: Brand" --filter campaign:IN:2144520245',
|
|
15
|
+
'<%= config.bin %> segments create --app UUID --title "US installs" --filter ip_country:IN:US --filter install_date:>=:2026-01-01',
|
|
16
|
+
];
|
|
17
|
+
static flags = { ...appIdFlag, ...segmentWriteFlags };
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(SegmentsCreate);
|
|
20
|
+
const body = {
|
|
21
|
+
description: flags.description,
|
|
22
|
+
filters: segmentFilters(flags),
|
|
23
|
+
title: flags.title,
|
|
24
|
+
};
|
|
25
|
+
const client = await createAuthenticatedClient(this.config);
|
|
26
|
+
const result = await client.post(`/apps/${flags.app}/segments`, body);
|
|
27
|
+
printResponse(result, this.log.bind(this));
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { SegmentDetailDTO } from '../../lib/api-schemas.js';
|
|
3
|
+
export default class SegmentsUpdate extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
segment_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static enableJsonFlag: boolean;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
filter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
title: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<SegmentDetailDTO>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { appIdFlag } from '../../cli/input/app.js';
|
|
3
|
+
import { segmentFilters, segmentWriteFlags } from '../../cli/input/segment.js';
|
|
4
|
+
import { createAuthenticatedClient } from '../../lib/client-from-config.js';
|
|
5
|
+
import { isValidUuid } from '../../lib/flags.js';
|
|
6
|
+
import { printResponse } from '../../lib/output.js';
|
|
7
|
+
export default class SegmentsUpdate extends Command {
|
|
8
|
+
static args = {
|
|
9
|
+
segment_id: Args.string({ description: 'Segment ID (UUID)', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = `Replace a segment's title, description and filters
|
|
12
|
+
|
|
13
|
+
The server takes the whole segment, as the dashboard sends it: pass every filter you want to keep, not only
|
|
14
|
+
the changed one. Read the current ones with \`segments get\`.`;
|
|
15
|
+
static enableJsonFlag = true;
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> segments update --app UUID 550e8400-e29b-41d4-a716-446655440000 --title "[ASA] Campaign: Brand" --filter campaign:IN:2144520245',
|
|
18
|
+
];
|
|
19
|
+
static flags = { ...appIdFlag, ...segmentWriteFlags };
|
|
20
|
+
async run() {
|
|
21
|
+
const { args, flags } = await this.parse(SegmentsUpdate);
|
|
22
|
+
if (!isValidUuid(args.segment_id)) {
|
|
23
|
+
this.error('Invalid segment ID format.', { exit: 2 });
|
|
24
|
+
}
|
|
25
|
+
const body = {
|
|
26
|
+
description: flags.description,
|
|
27
|
+
filters: segmentFilters(flags),
|
|
28
|
+
title: flags.title,
|
|
29
|
+
};
|
|
30
|
+
const client = await createAuthenticatedClient(this.config);
|
|
31
|
+
const result = await client.put(`/apps/${flags.app}/segments/${args.segment_id}`, body);
|
|
32
|
+
printResponse(result, this.log.bind(this));
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -136,6 +136,30 @@ export type SegmentDTO = {
|
|
|
136
136
|
id: string;
|
|
137
137
|
title: string;
|
|
138
138
|
};
|
|
139
|
+
export type SegmentFilterDTO = {
|
|
140
|
+
field_name: string;
|
|
141
|
+
operator: string;
|
|
142
|
+
segment_filter_id: null | string;
|
|
143
|
+
value_list: unknown[];
|
|
144
|
+
};
|
|
145
|
+
/** What `segments get`, `create` and `update` return; the list stays `SegmentDTO`. */
|
|
146
|
+
export type SegmentDetailDTO = SegmentDTO & {
|
|
147
|
+
created_at: string;
|
|
148
|
+
filters: SegmentFilterDTO[];
|
|
149
|
+
profile_count: number;
|
|
150
|
+
refresh_in_progress: boolean;
|
|
151
|
+
updated_at: string;
|
|
152
|
+
};
|
|
153
|
+
export type SegmentFilterInput = {
|
|
154
|
+
field_name: string;
|
|
155
|
+
operator: string;
|
|
156
|
+
value_list: unknown[];
|
|
157
|
+
};
|
|
158
|
+
export type SegmentWriteRequestDTO = {
|
|
159
|
+
description?: string | undefined;
|
|
160
|
+
filters: SegmentFilterInput[];
|
|
161
|
+
title: string;
|
|
162
|
+
};
|
|
139
163
|
export type PlacementPaywallAudienceEntryDTO = {
|
|
140
164
|
content_type: 'paywall';
|
|
141
165
|
paywall_id: string;
|
package/dist/lib/asa-client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiClient } from './api-client.js';
|
|
2
2
|
import type { QueryParams } from './api-client.js';
|
|
3
|
+
import type { AsaSegmentEntity, AsaSegmentSelection } from './asa-flags.js';
|
|
3
4
|
import type { Config } from '@oclif/core';
|
|
4
5
|
export declare const ASA_API_URL = "https://api-asa-admin.adapty.io/api/v1/cli";
|
|
5
6
|
export declare const ASA_API_URL_ENV_VAR = "ADAPTY_ASA_API_URL";
|
|
@@ -19,3 +20,5 @@ export type AsaWriteOutcome<T> = {
|
|
|
19
20
|
};
|
|
20
21
|
export declare function asaWrite<T>(client: ApiClient, method: 'post' | 'put', path: string, opts?: AsaWriteOptions): Promise<AsaWriteOutcome<T>>;
|
|
21
22
|
export declare function noteReplay(replayed: boolean, log: (msg: string) => void): void;
|
|
23
|
+
/** The ASA entities behind a segment source, with the Apple ids the portal filters on. */
|
|
24
|
+
export declare function fetchAsaSegmentEntities(client: ApiClient, selection: AsaSegmentSelection): Promise<AsaSegmentEntity[]>;
|