Skip to content

Publisher analytics: stats APIs and Studio Insights - #12

Merged
roncodes merged 28 commits into
release/v2.0.0from
feat/publisher-analytics
Oct 24, 2026
Merged

roncodes merged 28 commits into
release/v2.0.0from
feat/publisher-analytics

Conversation

@roncodes

Copy link
Copy Markdown
Member

Stacked PR. Based on feat/staff-finance (#11, top of #6 ← #8 ← #10 ← #11 into release/v2.0.0). Once the stack merges, retarget this to release/v2.0.0. Nothing here goes to main. Not deployed.

Publisher analytics: stats APIs for a listing and for an organization, and Studio Insights replacing the placeholder (design D14 Developer — Analytics).

API

Route Who Returns
GET /v1/publisher/extensions/{slug}/stats?from=&to=&group=day|week|month owner, admin, publisher (publish scope) extension, range, previous, as_of, totals, previous_totals, series, versions, events {data, total}
GET /v1/organizations/{slug}/stats?from=&to=&group= owner, admin, publisher, billing the same across every listing, with an extensions row per listing instead of versions
…/stats.csv (both) as above the series as CSV, gross, fee and net per currency
  • Totals: installs, updates, uninstalls, active instances, purchases, and revenue per currency (gross, refunded, fee, net), each compared with the previous period of the same length.
  • Defaults and limits: UTC days, the last 30 days by default, at most 731 days. The default grouping fits the range length.
  • Caching: reports are cached for 60 s (REGISTRY_ANALYTICS_CACHE_SECONDS); as_of says when one was built.

Data model

  • New columns on installs: installed_at, uninstalled_at, replaced_at and previous_release_uuid. Without them an update and an uninstall looked the same (both ended as uninstalled).
    • InstallController writes these columns.
    • So does the importer.
    • The migration backfills older rows. Updates made before it read as an uninstall plus an install; v2 has no live data yet.
  • installs_count stays derived from installs and now goes through Install::active(). The stats' active instances use the same scope, so the two always agree (tested).
  • Revenue follows staff Finance's rules: counted by sale date, with refunds taken off their sale.

Indexes, no rollup table

  • Indexes: installs (extension_uuid, installed_at, previous_release_uuid), (extension_uuid, uninstalled_at, installed_at) and (extension_uuid, status, release_uuid), which replaces (extension_uuid, status). Plus purchases (extension_uuid, created_at). The two lifecycle indexes are covering.
  • Active instances for a past range are walked back from today's count, so the report never scans a listing's whole history.
  • Measured on SQLite with 230k installs on one listing, on a heavily loaded machine:
    • 0.23 s for 30 days by day.
    • 0.9 s for two years by month.
    • 0.4 to 0.8 s for an organization of 50 listings.
  • Why no rollup: the expected scale is tens of listings, so a rollup table and scheduled command would add work with nothing to gain. The README documents the choice and when to add one.

Studio Insights (/studio/insights/{org?}?extension=&from=&to=&group=)

  • Filters: organization switcher, a listing picker (or All extensions), range presets (7/30/90 days, 1 year), a custom range and Group by.
  • Tiles: five, each with the change against the previous period.
  • Trend chart: installs and uninstalls in dependency-free SVG; the website had no chart library.
    • Uninstalls are dashed, and the colours are checked for colour-blind separation in light and dark.
    • Hover and arrow keys give a readout.
    • Partial first and last buckets are drawn faint.
    • "Show as a table" lists every value.
  • Panels: active installs by version (one listing) or a row per listing (all extensions); recent events with the organization, version and amount; CSV export.
  • Checked in the browser at desktop and 375 px, in light and dark.

Tests

  • Pest: 475 pass, 33 of them new (tests/Feature/Analytics, Insights pages in StudioPagesTest).
  • Vitest: 19 new tests in resources/js/components/insights. The full suite timed out on unrelated tests while the machine was overloaded (a different set each run). Every affected file passes on its own.

…d sale pricing; document the instance checkout contract
…m-side Stripe failures, highlight the selected organization on Billing
…voke the licence, staff licence management, return URLs on the instance host, members see licences
…otals, monthly CSV, refunds through Stripe, and the Finance page (Phase 4 M)
Installs record their lifecycle (installed_at, uninstalled_at, replaced_at and
previous_release_uuid), so installs, updates and uninstalls are told apart. The
stats read installs and purchases directly on covering indexes; no rollup table
(see the README). The catalog's installs_count stays derived from installs.
Listing or all-extensions picker, range presets and a custom range, tiles against
the previous period, a dependency-free SVG trend chart with a keyboard readout and
a table view, active installs by version, recent events and CSV export.
…st/{version} behind the licence gate, and dist size, sha256 and builtAgainst on catalog and update releases
# Conflicts:
#	app/Http/Controllers/Catalog/CatalogController.php
# Conflicts:
#	app/Models/Instance.php
@roncodes
roncodes changed the base branch from feat/staff-finance to release/v2.0.0 October 24, 2026 09:22
@roncodes
roncodes merged commit e50a896 into release/v2.0.0 Oct 24, 2026
6 checks passed
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