Skip to content

feat: experiment badge, target stats and license commands - #76

Merged
achoimet merged 7 commits into
mainfrom
feat/badge-stats-license
Sep 29, 2026
Merged

achoimet merged 7 commits into
mainfrom
feat/badge-stats-license

Conversation

@achoimet

Copy link
Copy Markdown
Member

Three read-only commands covering API areas the CLI didn't have yet. One commit each.

steadybit experiment badge -k KEY prints a ready-to-paste status badge for a README:

[![ADM-1039](https://platform.dev.steadybit.com/api/experiments/ADM-1039/badge.svg?tenantKey=demo)](https://platform.dev.steadybit.com/experiments/edit/ADM-1039?team=ADM&tenant=demo)
  • Flags:
    • --format markdown|html|url, with Markdown the default;
    • --scale;
    • --tag (with --create-caption) for the tag-linked badge, which invites to create an experiment while none has the tag;
    • --tenant;
    • -t json|yaml, which --jq applies to.
  • Nothing secret goes into a README. Badge URLs take the tenantKey, never the token. The command fetches the badge without the token, as a README would, to check the URL works. With a token, the platform ignores a wrong tenant key, so an authenticated check would pass on a badge that breaks in the README. This is the one change outside the new packages: platform.Client.GetAnonymously, which skips authorize, and a test asserts no Authorization header is sent. What a badge does expose: anyone with the tenant key (it's in every platform URL) can see that an experiment key exists and how its last run ended. The help says so.
  • A missing experiment's badge is a "not found" SVG with status 200, so the command looks the experiment up first, and fails with Experiment X not found. when there is none.
  • The tenant key comes from GET /api/license, the only endpoint that tells a token its tenant. That needs an admin token; otherwise pass --tenant.

steadybit target stats [-q QUERY]

  • Prints target counts per type (GET/POST /api/target-stats) as a table sorted by type, or -t json|yaml.
  • The API takes a query but no environment, so there's no -e.

steadybit license show / steadybit license report

  • show prints a one-line summary (license, tenant, validity, plus "expires in N days" when it's under 30), a table of the limits with their usage (red when over), and the included features.
  • report downloads the usage report, which is a zip. It's a subcommand rather than a flag because -t can't print binary data. It saves under the platform's filename, reduced to one path segment, or to -o FILE, as execution artifact download does.
  • A 403 says an admin token is needed.

Surprises from the API, handled:

  • The badge is a 200 SVG even for a missing experiment.
  • target-stats returns its map in random order, and the license's features too; both are sorted.
  • The license report is a zip, while the spec gives no content type.

Testing

  • Unit tests with the fake platform for every command: table, json/yaml, --jq, empty results, 403/404/422, a wrong tenant, no Authorization header on the badge check, and a report filename that can't leave the directory.
  • go test -race ./... and go vet (also GOOS=windows) pass.
  • Live on dev, read only: the badge Markdown above, and its image URL returns 200 image/svg+xml without a token. target stats counts 28,059 targets, and license show / license report work (the downloaded report was deleted).

Open questions

  • The badge link uses the platform's own UI URL form (/experiments/edit/KEY?tenant=…&team=…); the docs show a different one.
  • The 403 path is covered only by unit tests: the dev token is an admin token.

How many targets of each type the platform knows was only visible in the
UI's landscape. `target stats` prints the counts of the tenant, sorted by
type, or with -q only of the targets matching a target query, which is the
quick way to see what a query would hit before putting it into an
experiment or an environment. -t json|yaml and --jq get the platform's
object as it is.

The endpoint counts over the whole tenant and takes no environment, so
unlike `target query` there is no -e.
An admin had to open the UI to see when the license expires and how close
the tenant is to a limit such as services or environments. `license show`
prints the license, a warning when it expired or expires within 30 days,
the limited features with their usage, over-limit ones in red, and the
features that are simply included. -t json|yaml and --jq get the
platform's summary as it is.

The usage report is a zip archive holding one archive per license period
(the license as JSON, the usage as CSV), not a value -t could print, so it
is its own command, `license report`, which writes it like `execution
artifact download` writes artifacts: under the name the platform gives it,
reduced to one path segment, or to -o. Both need an admin access token,
and say so on 403 as `audit-log` does.
A status badge in a README shows whether an experiment's latest run
passed, but building its URL meant finding the badge dialog in the UI or
reading the docs for the tenantKey parameter. `experiment badge -k KEY`
prints the snippet: Markdown by default, --format html or url, --scale for
the image size; --tag prints the badge of a tag, which invites to create
the experiment while none has the tag. -t json|yaml and --jq get the
image URL, link and both snippets.

The badge endpoints are public and take only the tenant key, so nothing
secret ends up in a README; the help says what the badge shows to anyone
who knows that key. The access token does not name its tenant, and the
license summary is the only response that does, so the key is read from
it with an admin token, and --tenant gives it otherwise.

The platform answers a badge request made with a token for the token's
tenant, whatever tenantKey says, and a missing experiment's badge is an
image saying "not found" with 200. So the experiment is looked up first,
and the badge is fetched once without the token, as a README would load
it: a wrong tenant key fails the command instead of rendering as a broken
image.
Both built the request and set the User-Agent on their own, so a change to one could
miss the other. newRequest builds it; Get then adds the access token.
…ecks -t first

The badge of another tenant's experiment is an image saying "not found", sent with 200,
so the anonymous check passed and the command printed a broken badge. When the license
names the token's tenant, a --tenant that differs is refused; without an admin token
the license cannot be read and the given key is taken as before.

A wrong -t was only reported by the printing, after up to three requests; it is now
checked first, in target stats too. -t prints every format at once, so --format is
refused with it instead of being ignored, and --format no longer defaults to markdown
in the flag itself, which would make it look given.
…w reads right

license report wrote the name from Content-Disposition into the current directory and
replaced any file of that name, dotfiles included: the name is the platform's choice,
not the user's. It is now created only if it does not exist, a leading dot is dropped,
and a name reducing to nothing becomes license-report.zip. A file named with -o is
still overwritten, as asked.

license show left a double space for an empty order number and "of tenant ," for a
null tenant key; the sentence is built from the parts present. It said "1 days". The
limit shown was the first field present, so a soft-limit feature also sending a hard
limit showed the hard one; the feature's type now decides. Usage at a hard limit is
highlighted too, since nothing more can be added. A wrong -t is reported before the
request.
@achoimet
achoimet merged commit c7dd55c into main Sep 29, 2026
8 checks passed
@achoimet
achoimet deleted the feat/badge-stats-license branch September 29, 2026 13:14
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant