Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

371 linesCodeBlameRaw
1/**
2 * Costs & margin: the arithmetic behind the page, apart from the SVG and
3 * the Workers runtime so it can be tested under Node. Money is in micros.
4 */
5import type {
6 BillRead,
7 CloudflareCycle,
8 CostDay,
9 CostMappingInput,
10 CostSettings,
11 CycleMeter,
12 OverallMargin,
13 PauseLevel,
14 PlatformGuard,
15 SpendCaps,
16} from "@g1t/contracts";
17
18import { parseDollars, usd } from "./money.ts";
19
20/** Buckets Cloudflare does not bill: their cost is g1t's own figure. */
21export const NOT_CLOUDFLARE = new Set(["models"]);
22
23/** A day's cost: Cloudflare's bill, or g1t's own figure where Cloudflare does not bill it. */
24export function dayCost(day: CostDay): number {
25 return NOT_CLOUDFLARE.has(day.bucket) ? day.ownCostMicros : day.cfCostMicros;
26}
27
28export type DayFigures = { day: string; revenueMicros: number; costMicros: number };
29
30/** Every day from `since` to `until`, inclusive (YYYY-MM-DD, UTC). */
31export function daysBetween(since: string, until: string): string[] {
32 const start = Date.parse(`${since}T00:00:00Z`);
33 const end = Date.parse(`${until}T00:00:00Z`);
34 if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return [];
35 const out: string[] = [];
36 for (let at = start; at <= end && out.length < 400; at += 86_400_000) out.push(new Date(at).toISOString().slice(0, 10));
37 return out;
38}
39
40/**
41 * A day per day of the range, every day there even with nothing on it.
42 * All of g1t: money in (what workspaces paid, the plan too) against every
43 * cost. One product: what customers were charged for it at price against
44 * what it cost.
45 */
46export function daySeries(days: CostDay[], since: string, until: string, bucket: string | null): DayFigures[] {
47 const totals = new Map<string, DayFigures>(daysBetween(since, until).map((day) => [day, { day, revenueMicros: 0, costMicros: 0 }]));
48 for (const row of days) {
49 if (bucket && row.bucket !== bucket) continue;
50 const figures = totals.get(row.day);
51 if (!figures) continue;
52 figures.revenueMicros += bucket ? row.valueMicros : row.cashMicros;
53 figures.costMicros += dayCost(row);
54 }
55 return [...totals.values()];
56}
57
58/**
59 * The margin on the price a markup gives, in percent: cost plus 20% is a
60 * 16.7% margin, since the 20% is of the cost and the margin of the price.
61 */
62export function marginOnPrice(markupPercent: number): number {
63 return markupPercent > -100 ? (markupPercent / (100 + markupPercent)) * 100 : 0;
64}
65
66/**
67 * Who g1t paid over the range: Cloudflare's usage bill, its subscriptions
68 * (a month's, over the range) and the model providers. The same total as
69 * the statement's All in.
70 */
71export function whoPaid(
72 overall: { costMicros: number; cloudflareCostMicros?: number; modelsCostMicros?: number },
73 subscriptionsMicros: number,
74): { totalMicros: number; cloudflareMicros: number; subscriptionsMicros: number; modelsMicros: number } {
75 const modelsMicros = overall.modelsCostMicros ?? 0;
76 const cloudflareMicros = overall.cloudflareCostMicros ?? overall.costMicros - modelsMicros;
77 return { totalMicros: overall.costMicros + subscriptionsMicros, cloudflareMicros, subscriptionsMicros, modelsMicros };
78}
79
80/**
81 * Cloudflare's subscriptions over the range: billing's figure (each day its
82 * billing cycle's share, the same accrual as the month view), or from a
83 * billing that does not send one, a month's over 30 days.
84 */
85export function subscriptionsOver(monthlyMicros: number, days: number, overall?: Pick<OverallMargin, "subscriptionsMicros">): number {
86 if (typeof overall?.subscriptionsMicros === "number") return overall.subscriptionsMicros;
87 return Math.round((monthlyMicros * days) / 30);
88}
89
90/** What was given away, by why, for the statement: only the ones with any. */
91export function givenParts(o: OverallMargin): [string, number][] {
92 return (
93 [
94 ["100% discounts", o.givenCompedMicros ?? 0],
95 ["free use", o.givenFreeMicros ?? 0],
96 ["trial", o.givenTrialMicros ?? 0],
97 ["open-source pool", o.givenPoolMicros ?? 0],
98 ["partial discounts", o.givenDiscountMicros ?? 0],
99 ["promotional credit and complimentary allowances", o.givenCreditPromotionalMicros ?? 0],
100 ["goodwill credit", o.givenCreditGoodwillMicros ?? 0],
101 ["testing resets", o.givenResetMicros ?? 0],
102 ["charged without real money", o.givenUnpaidMicros ?? 0],
103 ] as [string, number][]
104 ).filter(([, micros]) => micros > 0);
105}
106
107/** How a cycle meter's cost was arrived at, in words. */
108export function basisLabel(basis: string): string {
109 if (basis === "cloudflare") return "Cloudflare's cost";
110 if (basis === "list") return "List price past the included amount";
111 return "No list price: counted at $0";
112}
113
114/** A cycle's headline: cost so far, the projection and the average day, as Cloudflare's Billable usage page puts them. */
115export function cycleHeadline(cycle: CloudflareCycle): { title: string; detail: string } {
116 return {
117 title: `${cycle.start} to ${cycle.end}, day ${cycle.daysElapsed} of ${cycle.days}`,
118 detail: `${usd(cycle.usageMicros, { cents: true })} so far, ${usd(cycle.averageDailyMicros, { cents: true })} a day; projected ${usd(cycle.projectedMicros, { cents: true })} for the cycle, and ${usd(cycle.subscriptionsMicros, { cents: true })} of subscriptions`,
119 };
120}
121
122/** Meters with no list price that were used: costed at $0 until one is added. */
123export function unpricedMeters(meters: CycleMeter[]): CycleMeter[] {
124 return meters.filter((m) => m.basis === "none" && m.quantity > 0);
125}
126
127/** What the last read of the bill got, in a sentence, and whether it looks incomplete. */
128export function billReadNote(read: BillRead): { text: string; warn: boolean } {
129 const parts = [`${read.rows.toLocaleString("en-US")} rows in ${read.pages} ${read.pages === 1 ? "page" : "pages"} for ${read.since} to ${read.until}`];
130 if (read.pricingOnlyRows > 0) parts.push(`${read.pricingOnlyRows} with only a pricing quantity, which can be in blocks`);
131 parts.push(read.costedRows > 0 ? `${read.costedRows} with Cloudflare's own cost` : "none with a cost of Cloudflare's, so the list prices apply");
132 return { text: `${parts.join("; ")}.`, warn: read.rows === 0 || read.pricingOnlyRows > 0 };
133}
134
135/**
136 * A price version's cost and price as the table shows them. A rate g1t
137 * sets has no cost behind it; a weight is a multiplier, not money.
138 */
139export function versionCells(v: { costMicros: number; priceMicros: number; basis?: string }): { cost: string; price: string; note: string | null } {
140 if (v.basis === "weight") {
141 const weight = Number((v.costMicros / 1_000_000).toFixed(6));
142 return { cost: "—", price: `×${weight}`, note: "A weight, not money (retired agent token pricing)" };
143 }
144 if (v.basis === "rate") return { cost: "—", price: unitDollars(v.priceMicros), note: "g1t's own rate: no cost behind it" };
145 return { cost: unitDollars(v.costMicros), price: unitDollars(v.priceMicros), note: null };
146}
147
148/**
149 * What became of a proposal, in words, for its line: who decided it, and
150 * when one never took effect because a later measurement replaced it.
151 */
152export function proposalOutcome(p: { status: string; decidedBy: string | null; effectiveAt: string | null }): string | null {
153 const by = p.decidedBy ? `${p.status === "superseded" ? "applied" : p.status} by ${p.decidedBy}` : null;
154 const replaced = "replaced by a later measurement before it took effect; nothing was charged at it";
155 if (p.status === "superseded") return by ? `${by}, then ${replaced}` : "replaced by a later measurement";
156 if ((p.status === "applied" || p.status === "approved") && !p.effectiveAt) return by ? `${by}, then ${replaced}` : replaced;
157 return by;
158}
159
160/** Margin as a whole percent of revenue; none when there was none. */
161export function marginPercent(revenueMicros: number, costMicros: number): number | null {
162 return revenueMicros > 0 ? ((revenueMicros - costMicros) / revenueMicros) * 100 : null;
163}
164
165/** `12.3%`, `−4.0%`, or a dash. */
166export function percentLabel(percent: number | null | undefined, { signed = false }: { signed?: boolean } = {}): string {
167 if (percent == null || !Number.isFinite(percent)) return "—";
168 const text = `${Math.abs(percent).toFixed(1)}%`;
169 if (percent < 0) return `−${text}`;
170 return signed && percent > 0 ? `+${text}` : text;
171}
172
173/** How a margin reads against the floor: under it is danger, close to it a warning. */
174export function marginTone(percent: number | null | undefined, floor: number): "danger" | "warn" | "mint" | undefined {
175 if (percent == null) return undefined;
176 if (percent < floor) return "danger";
177 if (percent < floor + 5) return "warn";
178 return "mint";
179}
180
181/** A count: `1,234` or `1.2M`. */
182export function countLabel(value: number): string {
183 if (!Number.isFinite(value)) return "—";
184 if (Math.abs(value) >= 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
185 return Math.round(value).toLocaleString("en-US");
186}
187
188/** What a kind of drift is called. */
189export function driftLabel(kind: string): string {
190 return { count: "Count", cost: "Cost", leak: "Leak" }[kind] ?? kind;
191}
192
193/** A cost per unit in micros, as dollars with as many places as it needs: `$0.000016`, `$0.15`. */
194export function unitDollars(micros: number): string {
195 const dollars = micros / 1_000_000;
196 if (dollars === 0) return "$0";
197 if (Math.abs(dollars) >= 1) return `$${dollars.toFixed(2)}`;
198 const places = Math.min(10, Math.max(2, 2 - Math.floor(Math.log10(Math.abs(dollars)))));
199 return `$${dollars.toFixed(places)}`;
200}
201
202/** The range shown: 7 to 90 days, 30 when not said. */
203export function parseRange(raw: string | null): number {
204 const days = Number(raw);
205 return Number.isInteger(days) && days >= 7 && days <= 90 ? days : 30;
206}
207
208/** The product to chart, if it is one the page has. */
209export function parseBucket(raw: string | null, known: string[]): string | null {
210 return raw && known.includes(raw) ? raw : null;
211}
212
213export type Parsed<T> = { ok: true; value: T } | { ok: false; error: string };
214
215function numberField(form: FormData, name: string): number {
216 const raw = String(form.get(name) ?? "").trim();
217 return raw === "" ? Number.NaN : Number(raw);
218}
219
220/** The guardrails from the settings form. */
221export function parseCostSettings(form: FormData): Parsed<CostSettings> {
222 const autoApplyPercent = numberField(form, "autoApplyPercent");
223 const noticeDays = numberField(form, "noticeDays");
224 const marginFloorPercent = numberField(form, "marginFloorPercent");
225 const alertDays = numberField(form, "alertDays");
226 const anomalyFactor = numberField(form, "anomalyFactor");
227 const minDaily = parseDollars(String(form.get("minDailyCost") ?? ""));
228 const anomalyFloor = parseDollars(String(form.get("anomalyFloor") ?? ""));
229 if (!(autoApplyPercent >= 0 && autoApplyPercent <= 100)) return { ok: false, error: "The guardrail is a percentage from 0 to 100." };
230 if (!(Number.isInteger(noticeDays) && noticeDays >= 0 && noticeDays <= 90)) return { ok: false, error: "Notice is 0 to 90 days." };
231 if (!(marginFloorPercent >= -100 && marginFloorPercent <= 100)) return { ok: false, error: "The margin floor is a percentage." };
232 if (!(Number.isInteger(alertDays) && alertDays >= 1 && alertDays <= 30)) return { ok: false, error: "Alert after 1 to 30 days." };
233 if (!(anomalyFactor > 0 && anomalyFactor <= 100)) return { ok: false, error: "The factor is a positive number." };
234 if (minDaily == null || anomalyFloor == null) return { ok: false, error: "Amounts are dollars to the cent." };
235 return {
236 ok: true,
237 value: {
238 autoApply: form.get("autoApply") === "on",
239 autoApplyPercent,
240 noticeDays,
241 marginFloorPercent,
242 alertDays,
243 minDailyCostMicros: minDaily,
244 anomalyFactor,
245 anomalyFloorMicros: anomalyFloor,
246 },
247 };
248}
249
250const NAME = /^(\*|[a-z0-9][a-z0-9_]{0,79})$/;
251
252/** A mapping from the mapping form: Cloudflare's product and meter (or `*`) to one of g1t's products. */
253export function parseMapping(form: FormData): Parsed<CostMappingInput> {
254 const value = (name: string) => String(form.get(name) ?? "").trim();
255 const product = value("product").toLowerCase();
256 const meter = value("meter").toLowerCase() || "*";
257 const remove = form.get("remove") === "1";
258 if (!NAME.test(product) || product === "*") return { ok: false, error: "Cloudflare's product, as the lines table names it." };
259 if (!NAME.test(meter)) return { ok: false, error: "A meter prefix as the lines table names it, or * for all of the product." };
260 if (remove) return { ok: true, value: { product, meter, remove: true } };
261 const bucket = value("bucket").toLowerCase();
262 if (!NAME.test(bucket) || bucket === "*") return { ok: false, error: "Which of g1t's products it is a cost of." };
263 const drift = value("driftPercent");
264 const driftPercent = drift === "" ? null : Number(drift);
265 if (driftPercent != null && !(driftPercent > 0 && driftPercent <= 1000)) return { ok: false, error: "Drift is a percentage above zero." };
266 return {
267 ok: true,
268 value: {
269 product,
270 meter,
271 bucket,
272 priceMeter: value("priceMeter") || null,
273 ownMeter: value("ownMeter") || null,
274 scaleToOwn: form.get("scaleToOwn") === "on",
275 driftPercent,
276 note: value("note").slice(0, 200),
277 },
278 };
279}
280
281// --- g1t's own spend (billing's budget) ---------------------------------------
282
283/**
284 * The red bar on every sudo page: the daily breaker open, or a 100%-discount
285 * account's monthly budget used up. Null when neither.
286 */
287export function spendBanner(caps: SpendCaps): string | null {
288 const parts: string[] = [];
289 if (caps.tripped) {
290 parts.push(`the daily breaker is open (${usd(caps.todayMicros)} of ${usd(caps.dailyCapMicros)} today), so new work g1t pays for waits until 00:00 UTC`);
291 }
292 for (const budget of caps.comped) {
293 if (budget.ceilingMicros > 0 && budget.usedMicros >= budget.ceilingMicros) {
294 parts.push(`${budget.name} used its ${usd(budget.ceilingMicros)} monthly budget, so new work on it is refused`);
295 }
296 }
297 if (parts.length === 0) return null;
298 const text = parts.join("; and ");
299 return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
300}
301
302/** What g1t paid this month, by bucket, with the free tier and Cloudflare's subscriptions; and the total. */
303export function spendRows(caps: SpendCaps): { rows: { key: string; title: string; micros: number; note: string }[]; totalMicros: number } {
304 const notes: Record<string, string> = {
305 comped: "Work on accounts with a 100% discount: what it cost g1t, not its price",
306 trial: "Trial credit, at what it cost g1t",
307 oss: "Checks and workflows on public repositories, at what they cost g1t",
308 given: "Free workspaces' overruns past their trial, at what they cost g1t",
309 unpaid: "Usage charged while payments are in Stripe's test mode: what it cost g1t, not what was charged",
310 };
311 const rows = caps.monthBuckets.map((b) => ({ key: b.bucket, title: b.title, micros: b.micros, note: notes[b.bucket] ?? "" }));
312 rows.push({ key: "free", title: "Free tier", micros: caps.freeTierMicros, note: "Free workspaces' share of git, storage and platform, as last reconciled" });
313 const monthly = usd(caps.fixedMonthlyMicros, { cents: true });
314 const accrued = typeof caps.fixedMonthMicros === "number";
315 rows.push({
316 key: "fixed",
317 title: "Cloudflare subscriptions",
318 micros: accrued ? (caps.fixedMonthMicros as number) : caps.fixedMonthlyMicros,
319 note: accrued
320 ? `This month's days so far, each its billing cycle's share of ${monthly} a month${caps.fixedSource === "cloudflare" ? ", as Cloudflare lists them" : ", estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)"}`
321 : caps.fixedSource === "cloudflare"
322 ? "The whole month, as Cloudflare lists them"
323 : "The whole month, estimated (CLOUDFLARE_FIXED_MONTHLY_MICROS)",
324 });
325 return { rows, totalMicros: rows.reduce((sum, row) => sum + row.micros, 0) };
326}
327
328/** How far a cap is used, 0 to 100, for a meter. */
329export function capPercent(usedMicros: number, capMicros: number): number {
330 if (capMicros <= 0) return 0;
331 return Math.max(0, Math.min(100, (usedMicros / capMicros) * 100));
332}
333
334// --- Platform pause and usage watch (billing's platform.rs) -----------------
335
336/** What each level of the platform pause stops, for the page and the banner. */
337export const PAUSE_LEVELS: { level: PauseLevel; title: string; stops: string }[] = [
338 { level: "compute", title: "Compute", stops: "New checks, workflow jobs and deploy builds, for every workspace. Runs already going finish." },
339 { level: "schedules", title: "Schedules", stops: "Actions' cron-triggered runs." },
340 { level: "indexing", title: "Indexing", stops: "Context embeddings and backfills, and search's backfills (they go on from where they were when resumed)." },
341 { level: "renders", title: "Renders", stops: "Social card images: a cache miss gets the brand card or the static logo." },
342];
343
344/** Whether a form's level is one of the four. */
345export function parsePauseLevel(value: unknown): PauseLevel | null {
346 const level = String(value ?? "");
347 return PAUSE_LEVELS.some((l) => l.level === level) ? (level as PauseLevel) : null;
348}
349
350/** The red bar on every sudo page while any level is paused. Null when none is. */
351export function pauseBanner(guard: PlatformGuard): string | null {
352 const paused = guard.levels.filter((l) => l.paused);
353 if (paused.length === 0) return null;
354 const names = paused.map((l) => (l.auto ? `${l.level} (by the usage watcher)` : l.level));
355 const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
356 return `Paused across g1t: ${list}.`;
357}
358
359/** `1.2M`, `240k`, `2.0B`: a count in a few characters. */
360export function count(value: number): string {
361 const n = Math.abs(value);
362 if (n >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
363 if (n >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
364 if (n >= 1e3) return `${Math.round(value / 1e3)}k`;
365 return `${Math.round(value)}`;
366}
367
368/** An hour's value as a share of its threshold, rounded; null with no threshold. */
369export function thresholdShare(value: number, threshold: number): number | null {
370 return threshold > 0 ? Math.round((value / threshold) * 100) : null;
371}