adapty 0.8.7 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +30 -2
  2. package/dist/cli/base/adapty/adapty-command.d.ts +1 -4
  3. package/dist/cli/base/adapty/build.d.ts +2 -8
  4. package/dist/cli/base/adapty/index.d.ts +2 -3
  5. package/dist/cli/base/adapty/index.js +1 -1
  6. package/dist/cli/base/adapty/openSession.d.ts +2 -19
  7. package/dist/cli/base/adapty/openSession.js +6 -16
  8. package/dist/cli/base/attribution/attribution-command.d.ts +16 -0
  9. package/dist/cli/base/attribution/attribution-command.js +42 -0
  10. package/dist/cli/base/attribution/build.d.ts +5 -0
  11. package/dist/cli/base/attribution/build.js +12 -0
  12. package/dist/cli/base/attribution/flags.d.ts +16 -0
  13. package/dist/cli/base/attribution/flags.js +28 -0
  14. package/dist/cli/base/attribution/index.d.ts +4 -0
  15. package/dist/cli/base/attribution/index.js +4 -0
  16. package/dist/cli/base/attribution/openSession.d.ts +9 -0
  17. package/dist/cli/base/attribution/openSession.js +19 -0
  18. package/dist/cli/base/base-command.d.ts +7 -0
  19. package/dist/cli/base/session.d.ts +30 -0
  20. package/dist/cli/base/session.js +20 -0
  21. package/dist/cli/commands/attribution/dimensions.d.ts +7 -0
  22. package/dist/cli/commands/attribution/dimensions.js +18 -0
  23. package/dist/cli/commands/attribution/metrics.d.ts +7 -0
  24. package/dist/cli/commands/attribution/metrics.js +35 -0
  25. package/dist/cli/commands/attribution/report/index.d.ts +18 -0
  26. package/dist/cli/commands/attribution/report/index.js +93 -0
  27. package/dist/cli/commands/attribution/report/lib/render.d.ts +3 -0
  28. package/dist/cli/commands/attribution/report/lib/render.js +24 -0
  29. package/dist/cli/commands/attribution/values.d.ts +14 -0
  30. package/dist/cli/commands/attribution/values.js +44 -0
  31. package/dist/cli/errors.d.ts +12 -0
  32. package/dist/cli/errors.js +16 -0
  33. package/dist/cli/flags.d.ts +4 -0
  34. package/dist/cli/flags.js +14 -4
  35. package/dist/commands/access-levels/create.js +2 -2
  36. package/dist/commands/access-levels/get.js +3 -2
  37. package/dist/commands/access-levels/list.js +3 -2
  38. package/dist/commands/access-levels/update.js +3 -2
  39. package/dist/commands/attribution/dimensions.d.ts +1 -0
  40. package/dist/commands/attribution/dimensions.js +2 -0
  41. package/dist/commands/attribution/metrics.d.ts +1 -0
  42. package/dist/commands/attribution/metrics.js +2 -0
  43. package/dist/commands/attribution/report.d.ts +1 -0
  44. package/dist/commands/attribution/report.js +2 -0
  45. package/dist/commands/attribution/values.d.ts +1 -0
  46. package/dist/commands/attribution/values.js +2 -0
  47. package/dist/commands/flows/config/get.js +3 -2
  48. package/dist/commands/flows/config/update.js +3 -2
  49. package/dist/commands/flows/config/validate.js +3 -2
  50. package/dist/commands/flows/create.js +2 -2
  51. package/dist/commands/flows/get.js +3 -2
  52. package/dist/commands/flows/list.js +3 -2
  53. package/dist/commands/flows/media/upload.js +2 -2
  54. package/dist/commands/flows/publish.js +3 -2
  55. package/dist/commands/flows/update.js +3 -2
  56. package/dist/commands/paywalls/create.js +2 -2
  57. package/dist/commands/paywalls/get.js +3 -2
  58. package/dist/commands/paywalls/list.js +3 -2
  59. package/dist/commands/paywalls/placements.js +3 -2
  60. package/dist/commands/paywalls/update.js +3 -2
  61. package/dist/commands/placements/create.js +2 -2
  62. package/dist/commands/placements/get.js +3 -2
  63. package/dist/commands/placements/list.js +3 -2
  64. package/dist/commands/placements/update.js +3 -2
  65. package/dist/commands/products/create.js +2 -2
  66. package/dist/commands/products/get.js +3 -2
  67. package/dist/commands/products/list.js +3 -2
  68. package/dist/commands/products/update.js +3 -2
  69. package/dist/commands/segments/get.js +3 -2
  70. package/dist/commands/segments/list.js +3 -2
  71. package/dist/lib/flags.d.ts +0 -3
  72. package/dist/lib/flags.js +0 -13
  73. package/dist/sdk/attribution/catalog.d.ts +51 -0
  74. package/dist/sdk/attribution/catalog.js +6 -0
  75. package/dist/sdk/attribution/errors.d.ts +10 -0
  76. package/dist/sdk/attribution/errors.js +43 -0
  77. package/dist/sdk/attribution/index.d.ts +30 -0
  78. package/dist/sdk/attribution/index.js +33 -0
  79. package/dist/sdk/attribution/report.d.ts +75 -0
  80. package/dist/sdk/attribution/report.js +81 -0
  81. package/dist/sdk/attribution/resource.d.ts +30 -0
  82. package/dist/sdk/attribution/resource.js +51 -0
  83. package/dist/sdk/attribution/values.d.ts +39 -0
  84. package/dist/sdk/attribution/values.js +21 -0
  85. package/oclif.manifest.json +461 -192
  86. package/package.json +4 -1
package/README.md CHANGED
@@ -387,6 +387,33 @@ too. A burst of 429s puts the token into an escalating cool-down (`cli_cooldown_
387
387
  not waiting out the pause in a loop. An automation run is queued rather than awaited: `run` prints a run ID
388
388
  and the outcome shows up in `adapty asa automations runs`.
389
389
 
390
+ ### Attribution
391
+
392
+ Attribution analytics across ad networks live under `adapty attribution` and talk to the attribution service
393
+ rather than the Developer API, with the same token. `report` and `values` take `--app`; the two catalogs take no
394
+ app.
395
+
396
+ ```sh
397
+ adapty attribution metrics # metric names a report can ask for
398
+ adapty attribution dimensions # dimensions a report can group or filter by
399
+ adapty attribution values --app APP_UUID --date-from 2026-08-01 --date-to 2026-08-31 --dimension campaign
400
+ adapty attribution report --app APP_UUID --date-from 2026-08-01 --date-to 2026-08-31 \
401
+ --metrics spend,installs,d7_roas --group-by date,campaign --granularity week \
402
+ --filter country=US,GB --revenue-basis proceeds --sort spend:desc
403
+ ```
404
+
405
+ Dates are inclusive days in the app's timezone. `--metrics` and `--group-by` take repeated flags or
406
+ comma-separated lists; `--filter dimension=value[,value]` repeats, where one value matches exactly and several
407
+ match any of them; `--sort field[:asc|desc]` is ascending unless told otherwise. `--json` prints the service's
408
+ answer unchanged (`{"success", "data", "meta"}`). In the human view a metric that cannot be computed prints
409
+ `—`, never `0`.
410
+
411
+ A rejected request exits 4 and carries the service's `error_code` in the `--json` error, e.g.
412
+ `attribution_unknown_metric` or `attribution_access_required` (the company has no attribution access — logging
413
+ in again does not help). `report` and `values` are sent once and never retried: on `attribution_busy` (429) or
414
+ an unavailable service (503), wait for the `Retry-After` the service sent before running the query again. The
415
+ `--json` error carries that wait as `retry_after_seconds`, whenever the service sent one.
416
+
390
417
  ### Global Flags
391
418
 
392
419
  | Flag | Description |
@@ -430,10 +457,11 @@ the flags and the size ceiling.
430
457
  | `ADAPTY_TOKEN` | Override stored auth token |
431
458
  | `ADAPTY_API_URL` | Override Developer API base URL (default: `https://api-admin.adapty.io/api/v1/developer`) |
432
459
  | `ADAPTY_ASA_API_URL` | Override Apple Search Ads base URL (default: `https://api-asa-admin.adapty.io/api/v1/cli`) |
460
+ | `ADAPTY_ATTRIBUTION_API_URL` | Override attribution base URL (default: `https://api-ua.adapty.io/api/v1/cli`) |
433
461
  | `ADAPTY_APP_URL` | Override dashboard base URL (default: `https://app.adapty.io`). Used by `flows config preview` for the fixed `/flow-preview` route, and by `auth login` to keep the verification link on that host |
434
462
 
435
- The two API URLs are independent: pointing `ADAPTY_API_URL` at a staging host leaves `adapty asa` on the ASA
436
- default, and the other way round.
463
+ The API URLs are independent: pointing `ADAPTY_API_URL` at a staging host leaves `adapty asa` and
464
+ `adapty attribution` on their own defaults, and the other way round.
437
465
 
438
466
  ## Claude Code Skill
439
467
 
@@ -1,9 +1,6 @@
1
1
  import { BaseCommand } from '../base-command.js';
2
- import type { ResolvedSession } from './openSession.js';
3
2
  import type { Adapty } from '../../../sdk/adapty/index.js';
4
- export type AuthenticatedSession = ResolvedSession & {
5
- token: string;
6
- };
3
+ import type { AuthenticatedSession } from '../session.js';
7
4
  /**
8
5
  * Commands that require a token; auth commands other than `whoami` also work without one.
9
6
  *
@@ -1,11 +1,5 @@
1
- import type { ResolvedSession } from './openSession.js';
2
1
  import type { Adapty } from '../../../sdk/adapty/index.js';
3
- import type { Config } from '@oclif/core';
4
- type CommandContext = {
5
- config: Config;
6
- signal: AbortSignal;
7
- warn: (message: string) => void;
8
- };
2
+ import type { CommandContext } from '../base-command.js';
3
+ import type { ResolvedSession } from '../session.js';
9
4
  /** Shared by authenticated commands and login/revoke, which can run without a token. */
10
5
  export declare const build: (session: ResolvedSession, context: CommandContext) => Adapty;
11
- export {};
@@ -1,5 +1,4 @@
1
1
  export { AdaptyCommand } from './adapty-command.js';
2
2
  export { build } from './build.js';
3
- export { openSession } from './openSession.js';
4
- export type { AuthenticatedSession } from './adapty-command.js';
5
- export type { ResolvedSession } from './openSession.js';
3
+ export { openSession, resolveSession } from './openSession.js';
4
+ export type { AuthenticatedSession, ResolvedSession } from '../session.js';
@@ -1,3 +1,3 @@
1
1
  export { AdaptyCommand } from './adapty-command.js';
2
2
  export { build } from './build.js';
3
- export { openSession } from './openSession.js';
3
+ export { openSession, resolveSession } from './openSession.js';
@@ -1,22 +1,5 @@
1
- import type { SessionStore, SessionUser } from '../../../sdk/core/session.js';
1
+ import type { ResolvedSession } from '../session.js';
2
2
  import type { Config } from '@oclif/core';
3
- /**
4
- * The CLI's world resolved into what the sdk asks for: where to talk, as whom, and where the
5
- * session lives. Env vars are read here and only here — the sdk never touches process.env, which
6
- * is what lets the same product code run under an MCP server.
7
- */
8
- export type ResolvedSession = {
9
- apiUrl: string;
10
- /**
11
- * Where the token came from. With ADAPTY_TOKEN in play, "no token in the file" and "not
12
- * authenticated" are different answers, and `auth status` has to say which one it means.
13
- */
14
- source: 'env' | 'file' | 'none';
15
- /** The store itself, not its path: `auth login`/`logout` write through it. */
16
- store: SessionStore;
17
- token?: string | undefined;
18
- user?: SessionUser | undefined;
19
- };
20
- /** ADAPTY_TOKEN wins over the stored session, as the published CLI already does. */
3
+ /** The shared token and store, sent to the developer API: ADAPTY_API_URL, or the default. */
21
4
  export declare const resolveSession: (config: Config) => Promise<ResolvedSession>;
22
5
  export declare const openSession: (config: Config) => Promise<ResolvedSession>;
@@ -1,20 +1,10 @@
1
1
  import { DEFAULT_ADAPTY_API_URL } from '../../../sdk/adapty/index.js';
2
- import { createFileSessionStore } from '../../../sdk/core/session.js';
3
- /** ADAPTY_TOKEN wins over the stored session, as the published CLI already does. */
4
- export const resolveSession = async (config) => {
5
- const apiUrl = process.env.ADAPTY_API_URL ?? DEFAULT_ADAPTY_API_URL;
6
- // oclif's configDir already knows XDG on unix and LOCALAPPDATA on Windows
7
- const store = createFileSessionStore(config.configDir);
8
- const envToken = process.env.ADAPTY_TOKEN;
9
- if (envToken !== undefined && envToken !== '') {
10
- return { apiUrl, source: 'env', store, token: envToken };
11
- }
12
- const stored = await store.load();
13
- if (stored === undefined) {
14
- return { apiUrl, source: 'none', store };
15
- }
16
- return { apiUrl, source: 'file', store, token: stored.token, user: stored.user };
17
- };
2
+ import { loadSession } from '../session.js';
3
+ /** The shared token and store, sent to the developer API: ADAPTY_API_URL, or the default. */
4
+ export const resolveSession = async (config) => ({
5
+ apiUrl: process.env.ADAPTY_API_URL ?? DEFAULT_ADAPTY_API_URL,
6
+ ...await loadSession(config),
7
+ });
18
8
  export const openSession = async (config) => {
19
9
  const session = await resolveSession(config);
20
10
  if (session.apiUrl !== DEFAULT_ADAPTY_API_URL) {
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ import type { Attribution } from '../../../sdk/attribution/index.js';
3
+ import type { AuthenticatedSession } from '../session.js';
4
+ /**
5
+ * Commands of the attribution backend, which all require a token. The same shape as AdaptyCommand:
6
+ * the session is resolved in init(), the token is demanded and the sdk built only on first access,
7
+ * so a command that validates its input first answers bad input with exit 2, not "Not authenticated".
8
+ */
9
+ export declare abstract class AttributionCommand extends BaseCommand {
10
+ #private;
11
+ static enableJsonFlag: boolean;
12
+ protected get attribution(): Attribution;
13
+ /** Narrowed once here, so nothing downstream re-checks the token. */
14
+ protected get session(): AuthenticatedSession;
15
+ protected init(): Promise<void>;
16
+ }
@@ -0,0 +1,42 @@
1
+ import { AuthRequiredError } from '../../../sdk/core/errors.js';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { build } from './build.js';
4
+ import { openSession } from './openSession.js';
5
+ /**
6
+ * Commands of the attribution backend, which all require a token. The same shape as AdaptyCommand:
7
+ * the session is resolved in init(), the token is demanded and the sdk built only on first access,
8
+ * so a command that validates its input first answers bad input with exit 2, not "Not authenticated".
9
+ */
10
+ export class AttributionCommand extends BaseCommand {
11
+ // Keep one own static so oclif's manifest cache walks through this intermediate class and
12
+ // includes statics inherited from BaseCommand.
13
+ static enableJsonFlag = true;
14
+ #attribution;
15
+ #resolved;
16
+ get attribution() {
17
+ this.#attribution ??= build(this.session, {
18
+ config: this.config,
19
+ signal: this.signal,
20
+ warn: message => this.warn(message),
21
+ });
22
+ return this.#attribution;
23
+ }
24
+ /** Narrowed once here, so nothing downstream re-checks the token. */
25
+ get session() {
26
+ const resolved = this.#resolved;
27
+ if (resolved === undefined) {
28
+ // A bug, not a user error, and deliberately not an SdkError: it lands as exit 1 with a
29
+ // stack instead of passing for a normal scenario.
30
+ throw new Error('session is available only after init()');
31
+ }
32
+ if (resolved.token === undefined) {
33
+ throw new AuthRequiredError('missing');
34
+ }
35
+ return { ...resolved, token: resolved.token };
36
+ }
37
+ async init() {
38
+ await super.init();
39
+ // Read here, demanded lazily: this.parse() runs after init(), and input errors come first.
40
+ this.#resolved = await openSession(this.config);
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ import type { Attribution } from '../../../sdk/attribution/index.js';
2
+ import type { CommandContext } from '../base-command.js';
3
+ import type { ResolvedSession } from '../session.js';
4
+ /** Mirrors the Adapty build: the retry warning is heard only on the catalog reads, the POSTs never retry. */
5
+ export declare const build: (session: ResolvedSession, context: CommandContext) => Attribution;
@@ -0,0 +1,12 @@
1
+ import { buildUserAgent } from '../../../lib/client-from-config.js';
2
+ import { createAttribution } from '../../../sdk/attribution/index.js';
3
+ /** Mirrors the Adapty build: the retry warning is heard only on the catalog reads, the POSTs never retry. */
4
+ export const build = (session, context) => createAttribution({
5
+ baseUrl: session.apiUrl,
6
+ onRetry: ({ attempt, delayMs }) => {
7
+ context.warn(`Request failed, retrying in ${delayMs / 1000}s (attempt ${attempt + 1})`);
8
+ },
9
+ signal: context.signal,
10
+ token: session.token,
11
+ userAgent: buildUserAgent(context.config),
12
+ });
@@ -0,0 +1,16 @@
1
+ /** An inclusive day range, both ends required. */
2
+ export declare const periodFlags: {
3
+ 'date-from': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
4
+ 'date-to': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
5
+ };
6
+ export declare const revenueBasisFlag: {
7
+ 'revenue-basis': import("@oclif/core/interfaces").OptionFlag<"gross" | "proceeds" | "net" | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ /** Where the period flags meet the sdk's field names. */
10
+ export declare const periodParams: (flags: {
11
+ "date-from": string;
12
+ "date-to": string;
13
+ }) => {
14
+ dateFrom: string;
15
+ dateTo: string;
16
+ };
@@ -0,0 +1,28 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { ISO_DATE, revenueBases } from '../../../sdk/attribution/index.js';
3
+ import { usageError } from '../../errors.js';
4
+ /** The shape only: whether the day exists and the order of the two are sdk rules. */
5
+ const dateParser = (input) => (ISO_DATE.test(input)
6
+ ? Promise.resolve(input)
7
+ : Promise.reject(usageError('Dates must be written as YYYY-MM-DD.')));
8
+ /** An inclusive day range, both ends required. */
9
+ export const periodFlags = {
10
+ 'date-from': Flags.string({
11
+ description: 'First day of the period, inclusive (YYYY-MM-DD, in the app timezone)',
12
+ parse: dateParser,
13
+ required: true,
14
+ }),
15
+ 'date-to': Flags.string({
16
+ description: 'Last day of the period, inclusive (YYYY-MM-DD, in the app timezone)',
17
+ parse: dateParser,
18
+ required: true,
19
+ }),
20
+ };
21
+ export const revenueBasisFlag = {
22
+ 'revenue-basis': Flags.option({
23
+ description: 'Which revenue the revenue metrics use; the backend default applies when omitted',
24
+ options: revenueBases,
25
+ })(),
26
+ };
27
+ /** Where the period flags meet the sdk's field names. */
28
+ export const periodParams = (flags) => ({ dateFrom: flags['date-from'], dateTo: flags['date-to'] });
@@ -0,0 +1,4 @@
1
+ export { AttributionCommand } from './attribution-command.js';
2
+ export { build } from './build.js';
3
+ export { periodFlags, periodParams, revenueBasisFlag } from './flags.js';
4
+ export { openSession, resolveAttributionSession } from './openSession.js';
@@ -0,0 +1,4 @@
1
+ export { AttributionCommand } from './attribution-command.js';
2
+ export { build } from './build.js';
3
+ export { periodFlags, periodParams, revenueBasisFlag } from './flags.js';
4
+ export { openSession, resolveAttributionSession } from './openSession.js';
@@ -0,0 +1,9 @@
1
+ import type { ResolvedSession } from '../session.js';
2
+ import type { Config } from '@oclif/core';
3
+ /**
4
+ * The attribution backend takes the Adapty developer token, so the token, its source, the store and
5
+ * the user are the shared session as it is. Only the destination is this product's own:
6
+ * ADAPTY_ATTRIBUTION_API_URL, independent of ADAPTY_API_URL.
7
+ */
8
+ export declare const resolveAttributionSession: (config: Config) => Promise<ResolvedSession>;
9
+ export declare const openSession: (config: Config) => Promise<ResolvedSession>;
@@ -0,0 +1,19 @@
1
+ import { DEFAULT_ATTRIBUTION_API_URL } from '../../../sdk/attribution/index.js';
2
+ import { loadSession } from '../session.js';
3
+ /**
4
+ * The attribution backend takes the Adapty developer token, so the token, its source, the store and
5
+ * the user are the shared session as it is. Only the destination is this product's own:
6
+ * ADAPTY_ATTRIBUTION_API_URL, independent of ADAPTY_API_URL.
7
+ */
8
+ export const resolveAttributionSession = async (config) => ({
9
+ apiUrl: process.env.ADAPTY_ATTRIBUTION_API_URL ?? DEFAULT_ATTRIBUTION_API_URL,
10
+ ...await loadSession(config),
11
+ });
12
+ export const openSession = async (config) => {
13
+ const session = await resolveAttributionSession(config);
14
+ if (session.apiUrl !== DEFAULT_ATTRIBUTION_API_URL) {
15
+ // oclif's warn() is silent under --json; the destination warning must remain visible.
16
+ process.stderr.write(`Warning: Using non-default attribution API URL: ${session.apiUrl}\n`);
17
+ }
18
+ return session;
19
+ };
@@ -1,5 +1,12 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import type { ErrorJson } from '../errors.js';
3
+ import type { Config } from '@oclif/core';
4
+ /** What a product's `build()` takes from the command that calls it. */
5
+ export type CommandContext = {
6
+ config: Config;
7
+ signal: AbortSignal;
8
+ warn: (message: string) => void;
9
+ };
3
10
  /**
4
11
  * What every command gets and nothing more: the output channel, cancellation, the single place
5
12
  * where sdk errors become CLI errors. Product SDKs and sessions belong to their adapters.
@@ -0,0 +1,30 @@
1
+ import type { SessionStore, SessionUser } from '../../sdk/core/session.js';
2
+ import type { Config } from '@oclif/core';
3
+ /**
4
+ * The CLI's world resolved into what the sdk asks for: where to talk, as whom, and where the
5
+ * session lives. Env vars are read in cli/base and only there — the sdk never touches process.env,
6
+ * which is what lets the same product code run under an MCP server.
7
+ */
8
+ export type ResolvedSession = {
9
+ apiUrl: string;
10
+ /**
11
+ * Where the token came from. With ADAPTY_TOKEN in play, "no token in the file" and "not
12
+ * authenticated" are different answers, and `auth status` has to say which one it means.
13
+ */
14
+ source: 'env' | 'file' | 'none';
15
+ /** The store itself, not its path: `auth login`/`logout` write through it. */
16
+ store: SessionStore;
17
+ token?: string | undefined;
18
+ user?: SessionUser | undefined;
19
+ };
20
+ /** What a product command holds once it has demanded the token. */
21
+ export type AuthenticatedSession = ResolvedSession & {
22
+ token: string;
23
+ };
24
+ /**
25
+ * Every product takes the same Adapty developer token, so as whom and where the session lives are
26
+ * resolved once, here. Where to talk is each product's own: its adapter adds `apiUrl`.
27
+ *
28
+ * ADAPTY_TOKEN wins over the stored session, as the published CLI already does.
29
+ */
30
+ export declare const loadSession: (config: Config) => Promise<Omit<ResolvedSession, "apiUrl">>;
@@ -0,0 +1,20 @@
1
+ import { createFileSessionStore } from '../../sdk/core/session.js';
2
+ /**
3
+ * Every product takes the same Adapty developer token, so as whom and where the session lives are
4
+ * resolved once, here. Where to talk is each product's own: its adapter adds `apiUrl`.
5
+ *
6
+ * ADAPTY_TOKEN wins over the stored session, as the published CLI already does.
7
+ */
8
+ export const loadSession = async (config) => {
9
+ // oclif's configDir already knows XDG on unix and LOCALAPPDATA on Windows
10
+ const store = createFileSessionStore(config.configDir);
11
+ const envToken = process.env.ADAPTY_TOKEN;
12
+ if (envToken !== undefined && envToken !== '') {
13
+ return { source: 'env', store, token: envToken };
14
+ }
15
+ const stored = await store.load();
16
+ if (stored === undefined) {
17
+ return { source: 'none', store };
18
+ }
19
+ return { source: 'file', store, token: stored.token, user: stored.user };
20
+ };
@@ -0,0 +1,7 @@
1
+ import { AttributionCommand } from '../../base/attribution/index.js';
2
+ import type { DimensionsResponse } from '../../../sdk/attribution/index.js';
3
+ export default class AttributionDimensions extends AttributionCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<DimensionsResponse>;
7
+ }
@@ -0,0 +1,18 @@
1
+ import { AttributionCommand } from '../../base/attribution/index.js';
2
+ const describeDimension = (dimension) => {
3
+ const uses = [dimension.groupable ? 'group' : undefined, dimension.filterable ? 'filter' : undefined]
4
+ .filter(use => use !== undefined);
5
+ const granularities = dimension.granularities === null ? '' : `; granularities ${dimension.granularities.join(', ')}`;
6
+ // identity says what a filter takes: an entity id, or the value itself
7
+ return `${dimension.name}: ${dimension.label} (${uses.join(', ') || 'no use'}; identity ${dimension.identity}${granularities})`;
8
+ };
9
+ export default class AttributionDimensions extends AttributionCommand {
10
+ static description = 'List the dimensions an attribution report can group or filter by (not scoped to an app)';
11
+ static examples = ['<%= config.bin %> attribution dimensions', '<%= config.bin %> attribution dimensions --json'];
12
+ async run() {
13
+ await this.parse(AttributionDimensions);
14
+ const catalog = await this.attribution.dimensions();
15
+ this.render(catalog, ({ data }) => data.dimensions.map(dimension => describeDimension(dimension)).join('\n'));
16
+ return catalog;
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ import { AttributionCommand } from '../../base/attribution/index.js';
2
+ import type { MetricsResponse } from '../../../sdk/attribution/index.js';
3
+ export default class AttributionMetrics extends AttributionCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ run(): Promise<MetricsResponse>;
7
+ }
@@ -0,0 +1,35 @@
1
+ import { AttributionCommand } from '../../base/attribution/index.js';
2
+ /** Traits under their catalog field names, so a reader knows what to look for in --json. */
3
+ const describeMetric = (metric) => {
4
+ const traits = [
5
+ metric.unit,
6
+ metric.spend_based ? 'spend_based' : undefined,
7
+ metric.denominator === null ? undefined : `denominator ${metric.denominator.join(' + ')}`,
8
+ ].filter(trait => trait !== undefined);
9
+ const pattern = metric.pattern === null
10
+ ? ''
11
+ : ` — pattern ${metric.pattern}${metric.example === null ? '' : `, e.g. ${metric.example}`}`;
12
+ return `${metric.name} (${traits.join(', ')})${pattern}\n ${metric.label}: ${metric.description}`;
13
+ };
14
+ const describeLimits = (limits) => {
15
+ const windows = Object.entries(limits.max_window_days)
16
+ .map(([grouping, days]) => `${grouping.replaceAll('_', ' ')} ${days}`)
17
+ .join(', ');
18
+ return [
19
+ `Limits: ${limits.max_metrics} metrics, ${limits.max_filter_values} values per filter, `
20
+ + `${limits.max_keyword_length} characters per keyword, ${limits.max_rows} rows`,
21
+ ` Widest period in days: ${windows}`,
22
+ ` Predictions: ${limits.max_prediction_horizons} horizons, day ${limits.max_prediction_day} at most, `
23
+ + `${limits.max_prediction_non_date_dimensions} dimensions besides date`,
24
+ ].join('\n');
25
+ };
26
+ export default class AttributionMetrics extends AttributionCommand {
27
+ static description = 'List the metrics an attribution report can ask for (not scoped to an app)';
28
+ static examples = ['<%= config.bin %> attribution metrics', '<%= config.bin %> attribution metrics --json'];
29
+ async run() {
30
+ await this.parse(AttributionMetrics);
31
+ const catalog = await this.attribution.metrics();
32
+ this.render(catalog, ({ data }) => [...data.metrics.map(metric => describeMetric(metric)), '', describeLimits(data.limits)].join('\n'));
33
+ return catalog;
34
+ }
35
+ }
@@ -0,0 +1,18 @@
1
+ import { AttributionCommand } from '../../../base/attribution/index.js';
2
+ import type { ReportFilter, ReportResponse, ReportSort } from '../../../../sdk/attribution/index.js';
3
+ export default class AttributionReport extends AttributionCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ sort: import("@oclif/core/interfaces").OptionFlag<ReportSort | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'revenue-basis': import("@oclif/core/interfaces").OptionFlag<"gross" | "proceeds" | "net" | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ metrics: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
10
+ 'group-by': import("@oclif/core/interfaces").OptionFlag<("store" | "date" | "campaign" | "adset" | "ad" | "keyword" | "channel" | "country")[], import("@oclif/core/interfaces").CustomOptions>;
11
+ granularity: import("@oclif/core/interfaces").OptionFlag<"day" | "week" | "month" | "quarter" | "year" | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ filter: import("@oclif/core/interfaces").OptionFlag<ReportFilter[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'date-from': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'date-to': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
16
+ };
17
+ run(): Promise<ReportResponse>;
18
+ }
@@ -0,0 +1,93 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { granularities, reportGroupBy, sortDirections, validateReport } from '../../../../sdk/attribution/index.js';
3
+ import { assertValid } from '../../../../sdk/core/validation.js';
4
+ import { AttributionCommand, periodFlags, periodParams, revenueBasisFlag } from '../../../base/attribution/index.js';
5
+ import { usageError } from '../../../errors.js';
6
+ import { appIdFlag } from '../../../flags.js';
7
+ import { renderReport } from './lib/render.js';
8
+ /** A comma not preceded by a backslash: `\,` keeps a comma inside a value, as oclif's own delimiter does. */
9
+ const VALUE_SEPARATOR = /(?<!\\),/;
10
+ /** `dimension=value[,value]`: one value filters by equality, several by any of them. */
11
+ const parseFilter = (input) => {
12
+ const separator = input.indexOf('=');
13
+ const dimension = separator === -1 ? '' : input.slice(0, separator).trim();
14
+ const values = separator === -1
15
+ ? []
16
+ : input.slice(separator + 1).split(VALUE_SEPARATOR).map(value => value.replaceAll('\\,', ',').trim())
17
+ .filter(value => value !== '');
18
+ return dimension === '' || values.length === 0
19
+ ? Promise.reject(usageError(`Expected dimension=value[,value], got "${input}".`))
20
+ : Promise.resolve({ dimension, values });
21
+ };
22
+ const isDirection = (value) => sortDirections.includes(value);
23
+ /** `field[:asc|desc]`, ascending when the direction is left out. */
24
+ const parseSort = (input) => {
25
+ const separator = input.lastIndexOf(':');
26
+ const field = (separator === -1 ? input : input.slice(0, separator)).trim();
27
+ const direction = separator === -1 ? 'asc' : input.slice(separator + 1).trim();
28
+ if (field === '') {
29
+ return Promise.reject(usageError(`Expected field[:asc|desc], got "${input}".`));
30
+ }
31
+ return isDirection(direction)
32
+ ? Promise.resolve({ direction, field })
33
+ : Promise.reject(usageError(`The sort direction must be asc or desc, got "${direction}".`));
34
+ };
35
+ export default class AttributionReport extends AttributionCommand {
36
+ static description = 'Run an Attribution report for an app: metrics over a period, grouped by dimensions';
37
+ static examples = [
38
+ '<%= config.bin %> attribution report --app APP_UUID --date-from 2026-08-01 --date-to 2026-08-31 --metrics spend,installs,d7_roas --group-by campaign',
39
+ '<%= config.bin %> attribution report --app APP_UUID --date-from 2026-08-01 --date-to 2026-08-31 --metrics spend --group-by date --granularity week --filter country=US,GB --sort spend:desc',
40
+ '<%= config.bin %> attribution report --app APP_UUID --date-from 2026-08-01 --date-to 2026-08-31 --metrics spend,installs,d7_roas --group-by campaign --json',
41
+ ];
42
+ // Input shape here; the rules (dates in order, a granularity exactly when grouping by date) live
43
+ // in sdk/attribution/report.ts, and which names exist is the backend catalog's knowledge.
44
+ static flags = {
45
+ ...appIdFlag,
46
+ ...periodFlags,
47
+ 'metrics': Flags.string({
48
+ delimiter: ',',
49
+ description: 'Metric names, as `adapty attribution metrics` lists them (repeatable or comma-separated)',
50
+ multiple: true,
51
+ required: true,
52
+ }),
53
+ 'group-by': Flags.option({
54
+ delimiter: ',',
55
+ description: 'Dimensions to group rows by (repeatable or comma-separated)',
56
+ multiple: true,
57
+ options: reportGroupBy,
58
+ required: true,
59
+ })(),
60
+ 'granularity': Flags.option({
61
+ description: 'Date bucket; required with --group-by date, and allowed only with it',
62
+ options: granularities,
63
+ })(),
64
+ 'filter': Flags.custom({
65
+ description: 'Keep rows whose dimension has one of the values: dimension=value[,value] (repeatable; `\\,` for a comma inside a value)',
66
+ multiple: true,
67
+ parse: parseFilter,
68
+ })(),
69
+ ...revenueBasisFlag,
70
+ 'sort': Flags.custom({
71
+ description: 'Sort rows by a requested metric or dimension: field[:asc|desc] (ascending by default)',
72
+ parse: parseSort,
73
+ })(),
74
+ };
75
+ async run() {
76
+ const { flags } = await this.parse(AttributionReport);
77
+ const input = {
78
+ appId: flags.app,
79
+ ...periodParams(flags),
80
+ filters: flags.filter,
81
+ granularity: flags.granularity,
82
+ groupBy: flags['group-by'],
83
+ metrics: flags.metrics.filter(metric => metric !== ''),
84
+ revenueBasis: flags['revenue-basis'],
85
+ sort: flags.sort,
86
+ };
87
+ // Before this.attribution: bad input is exit 2 even without a token
88
+ assertValid(validateReport(input));
89
+ const report = await this.attribution.report(input);
90
+ this.render(report, renderReport);
91
+ return report;
92
+ }
93
+ }
@@ -0,0 +1,3 @@
1
+ import type { ReportResponse } from '../../../../../sdk/attribution/index.js';
2
+ /** Private to `attribution report`: one labelled block per row, `---` between rows, then the totals. */
3
+ export declare const renderReport: (report: ReportResponse) => string;
@@ -0,0 +1,24 @@
1
+ /** A metric the backend could not compute is null, and printing it as 0 would be a lie. */
2
+ const NOT_COMPUTED = '—';
3
+ const formatValue = (value) => {
4
+ if (value === null || value === undefined) {
5
+ return NOT_COMPUTED;
6
+ }
7
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
8
+ return String(value);
9
+ }
10
+ return JSON.stringify(value);
11
+ };
12
+ /**
13
+ * Field names stay as the server sends them: they are the names `--metrics`, `--group-by`, `--filter`
14
+ * and `--sort` take, so what a reader sees is what they can type next.
15
+ */
16
+ const block = (record) => Object.entries(record).map(([name, value]) => `${name}: ${formatValue(value)}`);
17
+ /** Private to `attribution report`: one labelled block per row, `---` between rows, then the totals. */
18
+ export const renderReport = (report) => {
19
+ const { rows, totals } = report.data;
20
+ const body = rows.length === 0
21
+ ? 'No rows for this period.'
22
+ : rows.map(row => block(row).join('\n')).join('\n---\n');
23
+ return totals === null ? body : `${body}\n\n${['Totals', ...block(totals)].join('\n')}`;
24
+ };
@@ -0,0 +1,14 @@
1
+ import { AttributionCommand } from '../../base/attribution/index.js';
2
+ import type { ValuesResponse } from '../../../sdk/attribution/index.js';
3
+ export default class AttributionValues extends AttributionCommand {
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ 'revenue-basis': import("@oclif/core/interfaces").OptionFlag<"gross" | "proceeds" | "net" | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ dimension: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'date-from': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'date-to': 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<ValuesResponse>;
14
+ }