feat(ops): Business tab scaffolding (PR A — backend + raw JSON)#2076
Merged
aalemayhu merged 3 commits intofeat/ops-observabilityfrom May 9, 2026
Merged
feat(ops): Business tab scaffolding (PR A — backend + raw JSON)#2076aalemayhu merged 3 commits intofeat/ops-observabilityfrom
aalemayhu merged 3 commits intofeat/ops-observabilityfrom
Conversation
Adds /api/ops/business/metrics returning six business metrics (MRR, net new MRR MTD, active paying subs, 30d churn, 7d failed payments, 7d new paid conversions). Five come from the local subscriptions table via the new SubscriptionsAnalyticsRepository; failed payments hit Stripe with a 15-min in-memory cache. Owner-only via existing RequireOpsAccess. The Engineering view moves into a sibling EngineeringTab under a shared OpsLayout, with /ops/business rendering raw JSON in a <pre> as a sanity check before the card grid in PR B. Hourly Stripe sync added to ScheduleCleanup so the local subscriptions table stays fresh enough for the 15-min response cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the local-first approach (subscriptions table read) in favour of
calling Stripe directly with a 15-min server-side cache. Decouples the
dashboard from updateStripeSubscriptions, which Al kept manual-only.
- Delete SubscriptionsAnalyticsRepository + test
- BusinessMetricsService now owns all six metric queries via Stripe SDK
- mrr_usd and active_paying_subs share one paginated walk of
subscriptions.list({status: 'active'}) — assert in tests
- net_new_mrr_mtd_usd: subscriptions.list({created.gte: month_start})
- new_paid_conversions_7d: subscriptions.list({created.gte: 7d_ago})
- churn_30d_pct: subscriptions.search('canceled_at>:30d') / active count
- failed_payments_7d: invoices.list filtered to open + attempts > 0
- MRR normalization in TS: per-item unit_amount × quantity × factor
where factor is {month:1, year:1/12, week:4.33, day:30}/interval_count
- OpsRouter drops repository wiring; service constructed empty
- Tests now mock the Stripe SDK (external dep), cover yearly/weekly
normalization, multi-item subs, trialing exclusion, partial-failure
errors[], cache hit/expiry, single active-list walk
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stripe Search API uses range operators without the colon (`canceled_at>1234`), not `canceled_at>:1234`. The colon form is rejected with 'Ensure you have properly quoted values'. Adds a regex assertion on the search call so the syntax can't regress. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Contributor
Author
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What
Adds
/api/ops/business/metricsand a Business tab at/ops/businessrendering the raw JSON in a<pre>. The Engineering view moves into a siblingEngineeringTabunder a sharedOpsLayout, both lazy-loaded.This is PR A of the spec at
Documentation/ops-observability/STRIPE_SPEC.md. PR B will replace the<pre>with the six-card grid; no API churn there. Splitting catches Stripe-shape surprises (especially MRR computation) before the visual layer obscures them.Targets
feat/ops-observability(PR #2075) since that's where the existing/opsendpoint and middleware live; rebase tomainonce #2075 merges.Why
Spec: Al opens
/ops/businessand within 5s knows MRR, net new MRR MTD, active paying subs, 30d churn, 7d failed payments, 7d new paid conversions. Same-tab visibility makes "is this PR earning its complexity?" a daily question instead of a weekly one.300K-user goal alignment: the goal is gated on a sustainable revenue floor. Surfacing revenue health alongside engineering health turns Stripe from a once-a-week ritual into a glance.
How
Per the spec's
## Decisionsection, all six metrics are computed on demand from the Stripe API with a 15-min server-side cache. We considered reading from the localsubscriptionstable butupdateStripeSubscriptionsis manual-only by design — coupling the dashboard to deploy cadence was rejected.services/ops/BusinessMetricsService.ts— owns the Stripe SDK client and all six metric queries.Map<key, { value, expiresAt }>per-metric cache with a 15-min TTL.Promise.allof the six fetches; partial failures returnnullfor the failed metric and a top-levelerrors[]array.mrr_usdandactive_paying_subsshare one paginated walk ofsubscriptions.list({status: 'active'})— asserted in tests.net_new_mrr_mtd_usd:subscriptions.list({created: {gte: ts_month_start}})summed the same way.new_paid_conversions_7d:subscriptions.list({created: {gte: ts_7d}}), count.churn_30d_pct:subscriptions.search('canceled_at>:ts_30d')/ count from the active list (reused, not refetched).failed_payments_7d:invoices.list({collection_method: 'charge_automatically', created: {gte: ts_7d}})filtered tostatus='open' && attempt_count > 0.unit_amount × quantity × factor / interval_countwherefactor ∈ {month: 1, year: 1/12, week: 4.33, day: 30}. USD only.unit_amountis cents; divide by 100 at the boundary.OpsController.getBusinessMetricsmirrorsgetMetrics;RequireOpsAccessgates as before (404 for non-owners).OpsRouterconstructs the service with no dependencies.setInterval(updateStripeSubscriptions, ...)was added in an earlier draft and reverted at Al's call — the job stays manual-only. See## Follow-upin the spec for the conditions under which we'd revisit automating it.OpsLayout(h1 + tab bar +<Outlet />);EngineeringTabis the priorOpsPagebody;BusinessTabcallsuseBusinessMetrics(React Query, 30s refresh, same as Engineering) and dumps JSON. Tabs are<Link>s — path-based, reload-safe, screenshot-shareable./ops, per spec.Testing
BusinessMetricsService.test.ts— 9 cases: full shape, cache hit within 15m, refetch after 15m, yearly+weekly MRR normalization, trialing exclusion, multi-item sum, single active-list walk (assert call count), partial-failure errors[], invoice filter (onlyopen+attempt_count > 0).GetBusinessMetricsUseCase.test.ts— orchestration shape.OpsController.test.ts—getBusinessMetrics200 + 500 paths.OpsRouter.test.ts— boots the router on a real http server; 404 for non-owner, 200 + JSON shape for owner.BusinessTab.test.tsx— fetches/api/ops/business/metrics, renders JSON, shows error banner on 500.OpsLayout.test.tsx— both tabs render,aria-current="page"flips on/ops↔/ops/business.src/services/ops,src/usecases/ops,OpsController.test.ts,OpsRouter.test.ts.tsc --noEmitclean.Reconciliation gate (per spec)
Not yet performed locally. Before this PR moves out of draft, Al should:
pnpm devGET /api/ops/business/metricswhile logged in asalexander@alemayhu.com.mrr_usdto the Stripe Dashboard MRR. Spec gate: if drift >2%, flag and revisit before PR B.No local subscription sync is required — the service calls Stripe directly.
Risks
EngineeringTaband addsOpsLayout— reverting restores the prior single-page Engineering view cleanly.Goal alignment
Direct: surfaces revenue and churn on the same screen as engineering health, turning a weekly Stripe-dashboard check into a daily glance. Without a daily revenue signal, scaling toward 300K users is flying blind on whether new features pay rent.
Follow-ups (PR B and beyond)
<pre>with the six-card grid (3×2 desktop / 2×3 tablet / 1×6 mobile, pure frontend change).updateStripeSubscriptionslater — see## Follow-upinDocumentation/ops-observability/STRIPE_SPEC.md. Revisit when we know how stale the local mirror gets in practice and whether other features want it.