Skip to content

wb-26: adapty analytics, the analytics of an app from the Developer API - #51

Draft
kenaku wants to merge 12 commits into
mainfrom
wb-26-analytics-parity
Draft

kenaku wants to merge 12 commits into
mainfrom
wb-26-analytics-parity

Conversation

@kenaku

@kenaku kenaku commented Oct 12, 2026 •

Copy link
Copy Markdown

feat: adapty analytics, the analytics of an app from the Developer API. Spec: adaptyteam/headless-workbench#26.

Summary

A new topic adapty analytics reads the analytics of one app as the dashboard shows them, with the same developer token as the rest of the CLI: one login reads every app the token can access. All commands are read-only and take --app.

Command Required Optional
analytics catalog --app
analytics values --app, --dimension
analytics chart --app, --chart, --date-from, --date-to --compare-from/--compare-to, --group-by, --filter (repeatable), --granularity, --revenue-basis (money charts), --timezone, --csv
analytics conversion --app, --conversion, --date-from, --date-to --compare-from/--compare-to, --group-by, --filter, --granularity, --timezone, --csv
analytics funnel --app, --date-from, --date-to --group-by, --filter, --timezone, --csv
analytics retention --app, --date-from, --date-to --group-by, --filter, --use-trial, --timezone, --csv
analytics ltv --app, --date-from, --date-to --group-by, --filter, --revenue-basis, --period-type, --renewal-days, --timezone, --csv
analytics cohort --app, --date-from, --date-to --filter, --granularity, --revenue-basis, --period-type, --renewal-days, --timezone, --csv

Names and flags follow the attribution commands. Only chart and conversion take a compared period.

src/
├── commands/analytics/*.ts          # one-line re-exports (8)
├── cli/commands/analytics/*.ts      # flags, local checks, output choice
├── cli/views/analytics/             # human table (human.ts) and CSV (segments.ts), made from the answer
├── cli/input/{filter,period}.ts     # value splitting and day range, shared with attribution
├── sdk/adapty/analytics/            # resource on the Developer API, query rules
└── sdk/core/dates.ts                # date rule, shared with attribution

Outputs. Every data command answers in one shape: data.value (the number of the period), data.segments with the total first (key null), then one segment per group, and meta.query (the query that ran, with defaults and the time zone).

  • default: a short table for a human: a heading (what the answer is, the period, the time zone, the filters), then one row per group, total first, with the main numbers of the command and one column per point; money has 2 decimals; null prints as —; a compared period has its own table; an empty window prints No data for this period. and the summary;
  • --json: the API answer unchanged;
  • --csv: a table the CLI makes from that answer: one row per group, total first; a compared period in compare.* columns on the same row; --csv with --json is refused.

The CSV reads its fields from the answer, so a field the API adds later shows without a CLI change. Where the table leaves fields out, one line under it says that --json and --csv give every field.

The CLI keeps no list of chart or conversion names: which names exist is the server's knowledge (docs/architecture.md). analytics catalog lists them, and the API refuses an unknown name.

Group keys. A group key is the raw value that the API gives for the group. It is not promised to be a --filter value (a funnel group key is its title and can repeat; every group stays in the output, matched by position). Take filter values from analytics values. The total is found only by is_total, never by a null key. Details: docs/agent/analytics.md.

Exit codes.

Exit When
0 success
2 bad input, nothing sent: app UUID, dates (shape, real day, order), compare with both ends, --renewal-days without --period-type days (whole days, ascending, once), --filter without =, --csv with --json. The message names the flag.
3 no token: "Not authenticated. Run adapty auth login."; also a 403 (no access to the app)
4 a server refusal, with the API error code (unknown chart, conversion, grouping, filter key or time zone, --revenue-basis on a chart that is not about money; 429 with retry_after_seconds in --json; a success that is not the API envelope: malformed_response)

Also: the date rule, the day-range flag factory and the dimension=value splitting are now shared in src/cli/input/ and src/sdk/core/; each product keeps its own --filter parser (attribution behavior unchanged, its tests pass); docs (docs/agent/analytics.md, the skill, cli-commands.md, README, architecture), check-agent-docs for the new topic; version 0.10.0.

Evidence

  • Before: adapty analytics does not exist.
  • After: test/commands/analytics.test.ts and test/sdk/adapty/analytics/*.test.ts:
flags -> request body, per command
each exit-2 rule -> exit 2, message names the flag, fetch called 0 times
--csv: header + one row per group, total first; compare.* columns; empty window: header only, "No data" on stderr
human table: heading, one row per group, total first, money at 2 decimals, — for null; "No data for this period." with the summary
no token -> exit 3 with the login hint; server refusal -> exit 4 with the code; 429 -> exit 4 with the wait

pnpm build, pnpm check:agent-docs and pnpm test (with lint) pass: 805 tests.

Verification

The commands were run against the backend change on a test stand: the catalog, an empty window (No data for this period.), --json / --csv / human output for each data command, the local refusals and the exit codes. Results: see the evidence bundle in the spec issue adaptyteam/headless-workbench#26.

The last two commits (no drill-down, a group key is the raw backend value; every group kept when a key repeats in one period) were run on the stand: the checks for the catalog, empty windows, the three outputs, local refusals and exit codes pass, and a wrong chart or conversion name exits 4 with the API code.

Merge Danger

Door: two-way in code, one-way after a release

New commands only; attribution behavior is unchanged. Once 0.10.0 is published, the command names, flags, CSV columns and exit codes are a public interface that agents and scripts use.

Blast Radius: small

New topic. The shared input code touches the attribution report flags (same behavior, tests updated).

Owners: CODEOWNERS gives the new analytics paths to the default owners @mlnchk @gshteynardt. package.json and skills/adapty-cli/references/cli-commands.md have no owner.

Order: release after the Developer API analytics routes are live; until then the commands get 404.

Open points for the reviewer

  • A 403 exits 3, not 4. The CLI maps every HTTP 403 to exit 3 (existing mapping for all commands), so a token without access to the app exits 3 like a missing token. The existing mapping is kept and documented; the spec now says so (decision 11).
  • A wrong chart or conversion name costs one API call. The server refuses it with exit 4 and names the field; a new name in the API needs no CLI release.
  • A bad --timezone is not checked locally: the API refuses it (exit 4).
  • API numbers of cohort and LTV can differ in the last decimal digits between identical calls (backend float sums).

Sergey Bliznetsov and others added 12 commits October 12, 2026 19:29
Move the YYYY-MM-DD day and the inclusive day-range rule to sdk/core/dates.ts,
the --date-from/--date-to flags to cli/input/period.ts and the
dimension=value[,value] parser to cli/input/filter.ts, so a second product
(the Developer API analytics) can take them without importing
sdk/attribution. Attribution behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new topic with eight commands: catalog, values, and one data command per
operation (chart, conversion, funnel, retention, ltv, cohort). Each data
command prints a human view, the API answer unchanged (--json), or a CSV made
from it (--csv): one row per segment, the total first, a null value as an
empty cell. The human view keeps the total first, prints null as — and an
empty window as "No data for this period."

Bad input exits 2 before any request and names the flag: the app id, the
dates, the chart and conversion ids, a compared period with both ends (chart
and conversion only), --renewal-days only with --period-type days, --csv with
--json. A server refusal exits 4 with its error code; no token exits 3.

The sdk resource lives in sdk/adapty/analytics, one file per operation; the
views in cli/views/analytics. Docs: docs/agent/analytics.md, the skill and its
command reference, README, CLAUDE.md and docs/architecture.md; the agent docs
check now covers the analytics topic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… local revenue-basis rule

- --filter of the analytics commands sends values exactly as written; an
  empty value is a value, so --filter country= keeps the "No Data" group
  as the dashboard does. Attribution keeps its parser (trimmed, empty refused).
- The human view and the CSV read their fields from the answer, so every
  field the API gives is shown: nested objects as dotted names, list items by
  index; the CSV has a column per point field (<point>.<field>). This carries
  the new funnel step fields, the cohort money on three bases and predict
  sections, and the typed resolved query.
- --revenue-basis with a chart that is not about money exits 2 before any
  request.
- A success that is not the API envelope exits 4 with malformed_response.
- Tests: one table over the six commands, human and CSV; the fetch step
  helpers move to test/helpers; unused re-exports leave the doors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…input

- An answer without segments still shows every number of the period: the
  human view prints "No data for this period." and under it the summary
  (data fields, time zone, other meta fields such as recommended_period,
  null as —). The CSV gets one summary row per period (column row =
  summary) after the segment rows, so it keeps its header and invents no
  group row.
- cli/input keeps only what attribution and analytics share: the date
  parser, a day-range flag factory that takes the time-zone text, and the
  split of dimension=value[,value]. The attribution period flags and filter
  parser move to base/attribution/flags.ts, the exact analytics filter
  parser to base/adapty/flags.ts. Attribution behavior is unchanged.
- One naming rule: AnalyticsCatalog; the analytics flag helpers drop the
  analytics prefix. Test helpers are function declarations; the no-token
  test asserts the literal message instead of repeating a date case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The CSV has one row per group and the total first. A compared period is in
  compare.* columns of the same row (a group of one period only has empty
  cells in the other); the numbers of the whole answer (data.value,
  data.compare.value, meta.query.timezone, meta.recommended_period, ...)
  are in columns of every row under their JSON paths. No groups: stdout has
  the header line only, stderr "No data for this period." and the summary.
- The --csv help says how to tell the total (is_total, empty key) from a
  "No Data" group (key "").
- The human view and the CSV show the step number x of funnel steps and
  retention periods ("-2 Install").
- One helper turns groupBy and granularity into segmentation and
  period_unit for all six operations; conversion counts and the funnel value
  follow the API types (null where the backend has no number).
- The sdk rule message names no CLI flag; exports used only in their file
  are private; imports sorted; the exit-2 test asserts whole messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…umns, local renewal-days rule

- Carry the backend change: each catalog grouping names its filter (the
  filter key that takes its group keys, or null); the catalog human view
  shows it, and the docs say how to drill down (--filter <filter>=<key>).
  LTV cohort_size can be null.
- The CSV names each point column by x alone, the same for every segment; a
  step's title is a cell of its own (<x>.title). The human view prints the
  step number once ("2 Period", "-2 Install").
- --renewal-days must be whole days in ascending order, each once: refused
  with exit 2 before any request. An sdk message names another field as
  {fieldName} and the CLI writes the flag ("needs --period-type days").
- The CSV notice on stderr comes whenever the asked period has no segments,
  with the summary of each period.
- The six data commands print through one AnalyticsCommand.renderAnswer;
  the report --filter parser is back in its command.
- Tests: sdk table tests for the rules and the request; a conversion with a
  compared period; an asked period without groups beside a compared one;
  list messages asserted by the flag and a sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The drill-down docs and the segment key comment follow the backend: use
  the grouping's filter from the catalog of that operation or chart (null:
  no drill-down; period goes to renewal_period); the "No Data" key is the
  zero UUID for id groupings, no_offer for offer type and category,
  Uncategorized for duration, "" for other text groupings; no filter
  selects it for store_product_id, flow_screen_id and period; a comma inside
  a --filter value is written \,.
- Finished before the scope change: an sdk issue lists the fields its
  message names ({fieldName}) and only those are rewritten to flags; the
  compare messages name the other flag; the views read every field of
  meta.query from one source; the six data commands render with a view
  function and write the CSV notice to stderr (no base class); compare and
  grouping flags are mapped once; sdk table tests per source module;
  README says a 403 exits 3; the architecture doc lists every analytics file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chart and conversion names

The human view of the six data commands is a short table: one row per group,
the total first, money at 2 decimals, null as a dash. --json and --csv do not change.
The CLI no longer keeps local lists of chart and conversion ids: which names exist
is the backend's knowledge (docs/architecture.md); the API refuses a bad name (exit 4).
- The catalog no longer names a filter for a grouping: the type has no
  `filter`, and the human view lists the grouping keys only.
- A group `key` is the raw value that the backend gives, not a filter value:
  docs, skill reference, README, model and flag texts say so; the drill-down
  section and the "No Data" filter values are gone. `--filter` is unchanged
  (values exactly as written, an empty one too, `\,` for a comma).
- The total is found by `is_total` alone. A group with a null, "" or "None"
  key is a group of its own in the table and the CSV (one row, paired with
  its compared segment by the exact key).
- Tests: a table and CSV test with null, "" and "None" keys listed before
  the total; the funnel "No Data" group has the raw key null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A funnel group key is its title and can repeat. The CSV matched groups by raw key and kept
only the last one. A key that is unique in both periods is still matched by itself; a
repeated key is matched by position.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant