Skip to content

g1t/services/billing/src/margin.rs

1,749 lines79,991 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1//! What g1t earns on each thing it sells, measured against what
2//! Cloudflare actually charged for it.
3//!
4//! Once a day, after `costs` has read Cloudflare's bill, the reconciler
5//! puts three figures side by side for every day and each of g1t's
6//! products (a "bucket": sandboxes, deployments, git, repository storage,
7//! …):
8//!
9//! 1. **What Cloudflare charged**: the day's cost lines `cost_map` gives
10//! the bucket.
11//! 2. **What g1t's meters recorded**: the cost on the ledger's entries for
12//! it (the price book's cost at the time) and, where a mapping names
13//! one, g1t's own count of the same units (git operations).
14//! 3. **What customers were charged**: the entries' value at price, before
15//! the plan's included usage, a trial or a pool paid part of it; and of
16//! that, what workspaces paid. Month-end meters (git, storage, scans,
17//! embeddings, the actions cache) come from daily snapshots of what they
18//! had come to (`pending_days`). The plan's price is the `platform`
19//! bucket's: the plan pays for running g1t.
20//!
21//! From those: margin per product (value against cost) and for all of g1t
22//! (money in against every cost); drift (counts or costs that disagree past
23//! a mapping's threshold, and leaks: cost with no revenue, or a Cloudflare
24//! meter no one mapped); each workspace's cost, Cloudflare's figure shared
25//! out by each workspace's own meters; and price proposals when a unit's
26//! real cost has moved (`pricing`). Alerts go to staff by email and as a
27//! banner in sudo. See docs/BILLING_OPERATIONS.md.
28
29use std::collections::{BTreeMap, BTreeSet};
30
31use g1t_contracts::billing::*;
32use g1t_contracts::{FailureCode, Outcome, new_id};
33use g1t_contracts::time::rfc3339;
34use g1t_kit::now_ms;
35use serde::{Deserialize, Serialize};
36use worker::wasm_bindgen::JsValue;
37use worker::{Env, Result};
38
39use crate::Billing;
40use crate::costs::{self, ARTIFACTS_OPERATIONS, DAY_MS, Rule, SOURCE_ARTIFACTS, SOURCE_BILLABLE, UNMAPPED};
41
42/// Buckets that are the cost of running g1t, paid by the plan rather than
43/// sold by the unit: never a leak for having no revenue of their own.
44pub(crate) const OVERHEAD: [&str; 1] = ["platform"];
45/// Buckets Cloudflare does not bill: their cost is g1t's own figure.
46pub(crate) const NOT_CLOUDFLARE: [&str; 1] = ["models"];
47/// The days drift is judged over.
48const DRIFT_DAYS: u64 = 7;
49/// The days a workspace's cost is set against its revenue.
50const ANOMALY_DAYS: u64 = 30;
51/// The days a unit's cost is measured over.
52const MEASURE_DAYS: u64 = 30;
53/// Fewer of g1t's units than this say nothing about cost per unit.
54const MIN_UNITS: f64 = 1_000.0;
55/// An open alert is emailed again after this long.
56const REMIND_MS: u64 = 7 * DAY_MS;
57
58// ---------------------------------------------------------------------
59// The arithmetic, apart from the database so it can be tested.
60// ---------------------------------------------------------------------
61
62/// One of g1t's products on one day.
63#[derive(Clone, Debug, Default, PartialEq)]
64pub(crate) struct ProductDay {
65 pub day: String,
66 pub bucket: String,
67 /// What Cloudflare charged g1t, in millionths of a dollar.
68 pub cf_cost_micros: i64,
69 /// What g1t's meters recorded it cost (the price book's cost).
70 pub own_cost_micros: i64,
71 /// What customers were charged for it at price, before what paid.
72 pub value_micros: i64,
73 /// Of that, what workspaces paid themselves.
74 pub cash_micros: i64,
75 /// Units Cloudflare counted and units g1t counted, where a mapping
76 /// says they are the same units.
77 pub cf_quantity: f64,
78 pub own_quantity: f64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it79 /// Of `cost()`, what went on usage g1t gave away (the workspaces'
80 /// `WorkspaceDay::given`, added up).
81 pub given_micros: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily82}
83
84impl ProductDay {
85 /// What it cost: Cloudflare's figure where Cloudflare bills it, else
86 /// g1t's own (models are billed by their providers, through the gateway).
87 pub fn cost(&self) -> i64 {
88 if NOT_CLOUDFLARE.contains(&self.bucket.as_str()) { self.own_cost_micros } else { self.cf_cost_micros }
89 }
90}
91
92/// A line of Cloudflare's bill, as stored.
93#[derive(Clone, Debug, Deserialize)]
94pub(crate) struct LineRow {
95 pub day: String,
96 pub source: String,
97 pub product: String,
98 pub meter: String,
99 pub quantity: f64,
100 pub cost_usd: f64,
101}
102
103/// A count of g1t's own, as stored.
104#[derive(Clone, Debug, Deserialize)]
105pub(crate) struct OwnRow {
106 pub day: String,
107 pub meter: String,
108 pub workspace: String,
109 pub quantity: f64,
110}
111
112/// What a workspace was charged for one key on one day.
113#[derive(Clone, Debug, Default, PartialEq)]
114pub(crate) struct UsageRow {
115 pub day: String,
116 pub workspace: String,
117 /// A ledger task (or `builds`), a month-end source, or `plan`.
118 pub key: String,
119 pub value: i64,
120 pub cash: i64,
121 pub cost: i64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it122 /// Of `value`, what g1t gave away: all of it for g1t's own (comped)
123 /// workspaces and in a free period, else what the trial and the pools
124 /// paid. The Team plan's credit was paid for, so it is not given.
125 pub given: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily126}
127
128/// One workspace's share of a product's cost on one day.
129#[derive(Clone, Debug, PartialEq)]
130pub(crate) struct WorkspaceDay {
131 pub day: String,
132 pub workspace: String,
133 pub bucket: String,
134 pub cost: i64,
Margin alerts measure what is sold, and say dollars when a percentage would mislead135 /// What the workspace paid in cash.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily136 pub revenue: i64,
Margin alerts measure what is sold, and say dollars when a percentage would mislead137 /// What its usage was priced at, whoever paid for it.
138 pub value: i64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it139 /// Of `cost`, the part g1t gave away: the cost times the share of the
140 /// workspace's usage that day that g1t paid for (see `UsageRow::given`).
141 pub given: i64,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily142}
143
144fn micros(dollars: f64) -> i64 {
145 (dollars * 1_000_000.0).round() as i64
146}
147
148/// Puts the day's bill, g1t's counts and what customers were charged side
149/// by side, a row per day and bucket, and shares each bucket's cost out
150/// to workspaces.
151pub(crate) fn fold(
152 rules: &[Rule],
153 revenue_map: &BTreeMap<String, String>,
154 lines: &[LineRow],
155 own: &[OwnRow],
156 usage: &[UsageRow],
157) -> (Vec<ProductDay>, Vec<WorkspaceDay>) {
158 let mut days: BTreeMap<(String, String), ProductDay> = BTreeMap::new();
159 let entry = |day: &str, bucket: &str| -> ProductDay {
160 ProductDay { day: day.to_owned(), bucket: bucket.to_owned(), ..ProductDay::default() }
161 };
162 // Which of g1t's own meters count each bucket's units.
163 let mut own_meters: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
164 for rule in rules {
165 if let Some(meter) = &rule.own_meter {
166 own_meters.entry(rule.bucket.as_str()).or_default().insert(meter.as_str());
167 }
168 }
169 let mut events: BTreeMap<(String, String), f64> = BTreeMap::new();
170 for line in lines {
171 let rule = costs::classify(rules, &line.product, &line.meter);
172 let bucket = rule.map_or(UNMAPPED, |r| r.bucket.as_str());
173 let key = (line.day.clone(), bucket.to_owned());
174 if line.source == SOURCE_ARTIFACTS {
175 // What Artifacts counted: operations only, and only where the
176 // bill does not count them itself.
177 if ARTIFACTS_OPERATIONS.contains(&line.meter.as_str()) {
178 *events.entry(key).or_default() += line.quantity;
179 }
180 continue;
181 }
182 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
183 row.cf_cost_micros += micros(line.cost_usd);
184 if line.source == SOURCE_BILLABLE && rule.is_some_and(|r| r.own_meter.is_some()) {
185 row.cf_quantity += line.quantity;
186 }
187 }
188 for (key, quantity) in events {
189 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
190 if row.cf_quantity == 0.0 {
191 row.cf_quantity = quantity;
192 }
193 }
194 // g1t's own counts of the same units, by bucket and by workspace.
195 let mut own_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
196 // Cloudflare's own count by workspace, where it gives one
197 // (`cloudflare_<bucket>`): the best way to share its cost.
198 let mut cf_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
199 for count in own {
200 if let Some(bucket) = count.meter.strip_prefix("cloudflare_") {
201 cf_by.entry((count.day.clone(), bucket.to_owned())).or_default().push((count.workspace.clone(), count.quantity));
202 continue;
203 }
204 for (bucket, meters) in &own_meters {
205 if meters.contains(count.meter.as_str()) {
206 let key = (count.day.clone(), (*bucket).to_owned());
207 days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1)).own_quantity += count.quantity;
208 own_by.entry(key).or_default().push((count.workspace.clone(), count.quantity));
209 }
210 }
211 }
212 // What customers were charged.
213 let bucket_of = |key: &str| revenue_map.get(key).cloned().unwrap_or_else(|| "models".to_owned());
214 let mut value_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
215 let mut cost_by: BTreeMap<(String, String), Vec<(String, f64)>> = BTreeMap::new();
216 let mut revenue: BTreeMap<(String, String, String), i64> = BTreeMap::new();
Margin alerts measure what is sold, and say dollars when a percentage would mislead217 let mut valued: BTreeMap<(String, String, String), i64> = BTreeMap::new();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily218 let mut active: BTreeMap<String, Vec<(String, f64)>> = BTreeMap::new();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it219 let mut gave: BTreeMap<(String, String), (i64, i64)> = BTreeMap::new();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily220 for u in usage {
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it221 let g = gave.entry((u.day.clone(), u.workspace.clone())).or_default();
222 g.0 += u.given;
223 g.1 += u.value;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily224 let bucket = bucket_of(&u.key);
225 let key = (u.day.clone(), bucket.clone());
226 let row = days.entry(key.clone()).or_insert_with(|| entry(&key.0, &key.1));
227 row.own_cost_micros += u.cost;
228 row.value_micros += u.value;
229 row.cash_micros += u.cash;
230 value_by.entry(key.clone()).or_default().push((u.workspace.clone(), u.value as f64));
231 cost_by.entry(key).or_default().push((u.workspace.clone(), u.cost as f64));
Margin alerts measure what is sold, and say dollars when a percentage would mislead232 *revenue.entry((u.day.clone(), u.workspace.clone(), bucket.clone())).or_default() += u.cash;
233 *valued.entry((u.day.clone(), u.workspace.clone(), bucket)).or_default() += u.value;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily234 active.entry(u.day.clone()).or_default().push((u.workspace.clone(), u.value.max(u.cost) as f64));
235 }
236 // Each bucket's cost shared out: by Cloudflare's own count per
237 // workspace, else by g1t's own count of its units, else
238 // by what each workspace was charged for it, else by what its usage
239 // cost; running g1t, and what no one mapped, by each workspace's share
240 // of all usage that day.
241 let mut shares: BTreeMap<(String, String, String), i64> = BTreeMap::new();
242 for ((day, bucket), row) in &days {
243 let key = (day.clone(), bucket.clone());
244 let weigh = |m: &BTreeMap<(String, String), Vec<(String, f64)>>| m.get(&key).filter(|w| w.iter().any(|(_, v)| *v > 0.0)).cloned();
245 let weights = if OVERHEAD.contains(&bucket.as_str()) || bucket == UNMAPPED {
246 active.get(day).cloned()
247 } else {
248 weigh(&cf_by).or_else(|| weigh(&own_by)).or_else(|| weigh(&value_by)).or_else(|| weigh(&cost_by)).or_else(|| active.get(day).cloned())
249 };
250 for (workspace, micros) in attribute(row.cost(), &weights.unwrap_or_default()) {
251 *shares.entry((day.clone(), workspace, bucket.clone())).or_default() += micros;
252 }
253 }
254 let keys: BTreeSet<(String, String, String)> = shares.keys().chain(revenue.keys()).cloned().collect();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it255 let workspaces: Vec<WorkspaceDay> = keys
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily256 .into_iter()
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it257 .map(|(day, workspace, bucket)| {
258 let cost = shares.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0);
259 // The day's share given away applies to every bucket, so a
260 // comped workspace's part of running g1t is given too.
261 let given = match gave.get(&(day.clone(), workspace.clone())) {
262 Some(&(given, value)) if value > 0 => (cost as i128 * given.clamp(0, value) as i128 / value as i128) as i64,
263 _ => 0,
264 };
265 WorkspaceDay {
266 cost,
267 revenue: revenue.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
268 value: valued.get(&(day.clone(), workspace.clone(), bucket.clone())).copied().unwrap_or(0),
269 given,
270 day,
271 workspace,
272 bucket,
273 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily274 })
275 .collect();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it276 for w in &workspaces {
277 if let Some(row) = days.get_mut(&(w.day.clone(), w.bucket.clone())) {
278 row.given_micros += w.given;
279 }
280 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily281 (days.into_values().collect(), workspaces)
282}
283
284/// A month-end source's day, from the snapshots of what it had come to:
285/// each day's figure less the day before's in the same month (the first
286/// day of a month, or the first snapshot, is its own).
287pub(crate) fn pending_deltas(snapshots: &[(String, String, String, i64, i64)]) -> Vec<UsageRow> {
288 // (day, workspace, source, cost, charge), any order.
289 let mut sorted = snapshots.to_vec();
290 sorted.sort_by(|a, b| (&a.1, &a.2, &a.0).cmp(&(&b.1, &b.2, &b.0)));
291 let mut out = Vec::new();
292 let mut previous: Option<&(String, String, String, i64, i64)> = None;
293 for snap in &sorted {
294 let (day, workspace, source, cost, charge) = snap;
295 let (before_cost, before_charge) = match previous {
296 Some(p) if p.1 == *workspace && p.2 == *source && p.0[..7] == day[..7] => (p.3, p.4),
297 _ => (0, 0),
298 };
299 let (cost, charge) = ((cost - before_cost).max(0), (charge - before_charge).max(0));
300 if cost > 0 || charge > 0 {
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it301 out.push(UsageRow { day: day.clone(), workspace: workspace.clone(), key: source.clone(), value: charge, cash: charge, cost, given: 0 });
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily302 }
303 previous = Some(snap);
304 }
305 out
306}
307
308/// Margin as a share of what was charged, in percent; None when nothing was.
309pub(crate) fn margin_percent(revenue_micros: i64, cost_micros: i64) -> Option<f64> {
310 (revenue_micros > 0).then(|| (revenue_micros - cost_micros) as f64 * 100.0 / revenue_micros as f64)
311}
312
313/// How far `ours` is from `theirs`, in percent of theirs; None when theirs
314/// is nothing.
315pub(crate) fn delta_percent(ours: f64, theirs: f64) -> Option<f64> {
316 (theirs > 0.0).then(|| (ours - theirs) * 100.0 / theirs)
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320pub(crate) enum DriftKind {
321 /// g1t counted a different number of units than Cloudflare did.
322 Count,
323 /// What Cloudflare charged differs from what the price book says the
324 /// same usage cost.
325 Cost,
326 /// Cloudflare charged for something nothing charges customers for.
327 Leak,
328}
329
330impl DriftKind {
331 pub fn as_str(self) -> &'static str {
332 match self {
333 DriftKind::Count => "count",
334 DriftKind::Cost => "cost",
335 DriftKind::Leak => "leak",
336 }
337 }
338}
339
340#[derive(Clone, Debug, PartialEq)]
341pub(crate) struct Drift {
342 pub bucket: String,
343 pub kind: DriftKind,
344 pub ours: f64,
345 pub cloudflare: f64,
346 pub delta_percent: Option<f64>,
347}
348
349/// Drift over a window for one bucket: counts more than `threshold`
350/// percent apart, a bill that far from the price book's cost of the same
351/// usage, and cost with nothing charged for it. Under `min_cost_micros`
352/// in all, cost says nothing.
353pub(crate) fn drifts(bucket: &str, days: &[ProductDay], threshold: f64, counted: bool, min_cost_micros: i64) -> Vec<Drift> {
354 let overhead = OVERHEAD.contains(&bucket);
355 let sum = |f: &dyn Fn(&ProductDay) -> f64| days.iter().map(f).sum::<f64>();
356 let cf_cost = sum(&|d| d.cf_cost_micros as f64);
357 let own_cost = sum(&|d| d.own_cost_micros as f64);
358 let value = sum(&|d| d.value_micros as f64);
359 let (cf_quantity, own_quantity) = (sum(&|d| d.cf_quantity), sum(&|d| d.own_quantity));
360 let mut out = Vec::new();
361 if counted && cf_quantity > 0.0 {
362 let delta = delta_percent(own_quantity, cf_quantity);
363 if delta.is_some_and(|d| d.abs() > threshold) {
364 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Count, ours: own_quantity, cloudflare: cf_quantity, delta_percent: delta });
365 }
366 }
367 let enough = cf_cost.max(own_cost) >= min_cost_micros as f64;
368 if enough && !overhead && cf_cost > 0.0 && own_cost > 0.0 && !NOT_CLOUDFLARE.contains(&bucket) {
369 let delta = delta_percent(own_cost, cf_cost);
370 if delta.is_some_and(|d| d.abs() > threshold) {
371 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Cost, ours: own_cost, cloudflare: cf_cost, delta_percent: delta });
372 }
373 }
374 if !overhead && cf_cost >= min_cost_micros as f64 && value <= 0.0 {
375 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Leak, ours: value, cloudflare: cf_cost, delta_percent: None });
376 }
377 out
378}
379
380/// When the last `days` in a row (each with enough cost to say something)
381/// were all under the floor: the first of them and the worst margin.
382/// Each item is a day's (day, revenue, cost).
383pub(crate) fn breach(series: &[(String, i64, i64)], floor_percent: f64, days: usize, min_cost_micros: i64) -> Option<(String, f64)> {
384 if days == 0 || series.len() < days {
385 return None;
386 }
387 let tail = &series[series.len() - days..];
388 let mut worst = f64::INFINITY;
389 for (_, revenue, cost) in tail {
390 if *cost < min_cost_micros {
391 return None;
392 }
393 let margin = margin_percent(*revenue, *cost).unwrap_or(-100.0);
394 if margin >= floor_percent {
395 return None;
396 }
397 worst = worst.min(margin);
398 }
399 Some((tail[0].0.clone(), worst))
400}
401
402/// `total` shared out in proportion to `weights`, in whole millionths that
403/// add up to it exactly (largest remainder first). Nothing to share, or no
404/// weight, shares nothing.
405pub(crate) fn attribute(total: i64, weights: &[(String, f64)]) -> Vec<(String, i64)> {
406 let mut merged: BTreeMap<String, f64> = BTreeMap::new();
407 for (key, w) in weights {
408 *merged.entry(key.clone()).or_default() += w.max(0.0);
409 }
410 let sum: f64 = merged.values().sum();
411 if total <= 0 || sum <= 0.0 {
412 return Vec::new();
413 }
414 let mut shares: Vec<(String, i64, f64)> = merged
415 .into_iter()
416 .map(|(key, w)| {
417 let exact = total as f64 * w / sum;
418 (key, exact.floor() as i64, exact - exact.floor())
419 })
420 .collect();
421 let mut left = total - shares.iter().map(|s| s.1).sum::<i64>();
422 let mut order: Vec<usize> = (0..shares.len()).collect();
423 order.sort_by(|a, b| shares[*b].2.total_cmp(&shares[*a].2).then(shares[*a].0.cmp(&shares[*b].0)));
424 for index in order {
425 if left <= 0 {
426 break;
427 }
428 shares[index].1 += 1;
429 left -= 1;
430 }
431 shares.into_iter().filter(|s| s.1 > 0).map(|(key, micros, _)| (key, micros)).collect()
432}
433
434/// Workspaces that cost g1t more than `factor` times what they paid, with
435/// at least `floor_micros` of cost: each (workspace, cost, revenue), the
436/// biggest gap first.
Models' margin read -14%: usage nothing paid for is valued at price, not $0437/// What a day's usage was worth at price. g1t's own workspaces are valued
438/// at price. So is usage nothing paid for, neither charged nor drawn from
439/// the plan, a trial, a pool or a gift (a free period): it was given away at
440/// its price, not sold for nothing. Anything paid keeps what it was paid, so
441/// a discount still shows as one.
442pub(crate) fn usage_value(internal: bool, cost: i64, paid: i64, margin_percent: u32) -> i64 {
443 if internal || (paid == 0 && cost > 0) {
444 return crate::credits::with_margin(cost, margin_percent);
445 }
446 paid
447}
448
Margin alerts measure what is sold, and say dollars when a percentage would mislead449/// What the overall alert says: the money as money, and a percentage only
450/// while there is enough coming in for one to mean something (a few cents
451/// against dollars of cost reads as -8000%).
452pub(crate) fn overall_detail(took: i64, spent: i64, days: usize, floor: f64, worst: f64) -> String {
453 if took < 1_000_000 * days as i64 {
454 return format!(
455 "All of g1t, comped workspaces left out: took in {} against {} of Cloudflare's bill over {days} days.",
456 dollars(took),
457 dollars(spent)
458 );
459 }
460 format!("All of g1t, comped workspaces left out: money in against Cloudflare's bill under {floor:.0}% for {days} days running, as low as {worst:.1}%.")
461}
462
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily463pub(crate) fn anomalies(rows: &[(String, i64, i64)], factor: f64, floor_micros: i64) -> Vec<(String, i64, i64)> {
464 let mut out: Vec<(String, i64, i64)> = rows
465 .iter()
466 .filter(|(_, cost, revenue)| *cost >= floor_micros && *cost as f64 > *revenue as f64 * factor)
467 .cloned()
468 .collect();
469 out.sort_by(|a, b| (b.1 - b.2).cmp(&(a.1 - a.2)).then(a.0.cmp(&b.0)));
470 out
471}
472
473/// Cloudflare's marginal rate for one of its units: the median over the
474/// charged days of cost over quantity, in dollars. None while the included
475/// amounts still cover it. Each item is a day's (quantity, cost).
476pub(crate) fn billed_rate(days: &[(f64, f64)]) -> Option<f64> {
477 let mut rates: Vec<f64> = days.iter().filter(|(q, c)| *q > 0.0 && *c > 0.0).map(|(q, c)| c / q).collect();
478 if rates.is_empty() {
479 return None;
480 }
481 rates.sort_by(f64::total_cmp);
482 Some(rates[rates.len() / 2])
483}
484
485/// What one of g1t's units costs, from Cloudflare's rate per its own unit
486/// and how many of Cloudflare's units each of g1t's took: if Cloudflare
487/// counts three operations for every git operation g1t counts, a git
488/// operation costs three of Cloudflare's. None without enough of g1t's
489/// units to say.
490pub(crate) fn derived_unit_cost(rate_per_cf_unit: f64, cf_units: f64, own_units: f64) -> Option<f64> {
491 (own_units >= MIN_UNITS && cf_units > 0.0 && rate_per_cf_unit > 0.0).then(|| rate_per_cf_unit * cf_units / own_units)
492}
493
494/// How many units a price is per: `1,000 operations` → 1,000, `million
495/// requests` → 1,000,000, `second` → 1.
496pub(crate) fn unit_size(unit: &str) -> f64 {
497 let first = unit.split_whitespace().next().unwrap_or_default().replace(',', "");
498 match first.as_str() {
499 "million" => 1_000_000.0,
500 "thousand" => 1_000.0,
501 n => n.parse().unwrap_or(1.0),
502 }
503}
504
505fn day_before(day: &str, days: u64) -> String {
506 let ms = g1t_contracts::time::parse_rfc3339(&format!("{day}T00:00:00Z")).unwrap_or(0);
507 rfc3339(ms.saturating_sub(days * DAY_MS))[..10].to_owned()
508}
509
510/// Dollars to the cent from a dollar up, finer below: `$17.02`, `$0.063`.
511fn dollars(micros: i64) -> String {
512 if micros.abs() >= 1_000_000 {
513 let cents = (micros as f64 / 10_000.0).round() as i64;
514 format!("{}${}.{:02}", if cents < 0 { "-" } else { "" }, cents.abs() / 100, cents.abs() % 100)
515 } else {
516 crate::features::dollars(micros)
517 }
518}
519
520/// The days a plan payment is spread over.
521const PLAN_DAYS: u64 = 30;
522
523/// `micros` paid on `day` spread evenly over `days` days from it, in
524/// whole micros that add up to it (the first days take the remainder).
525pub(crate) fn spread(day: &str, micros: i64, days: u64) -> Vec<(String, i64)> {
526 if micros <= 0 || days == 0 {
527 return Vec::new();
528 }
529 let start = g1t_contracts::time::parse_rfc3339(&format!("{}T00:00:00Z", &day[..10.min(day.len())])).unwrap_or(0);
530 let each = micros / days as i64;
531 let rest = micros % days as i64;
532 (0..days)
533 .map(|n| (rfc3339(start + n * DAY_MS)[..10].to_owned(), each + i64::from((n as i64) < rest)))
534 .collect()
535}
536
537// ---------------------------------------------------------------------
538// The daily run, and what sudo reads.
539// ---------------------------------------------------------------------
540
541#[derive(Serialize)]
542struct Mail<'a> {
543 to: &'a str,
544 from: &'a str,
545 subject: &'a str,
546 text: String,
547 html: String,
548}
549
550fn escape(text: &str) -> String {
551 text.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")
552}
553
554/// Emails staff through Cloudflare Email Sending, the `EMAIL` binding.
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays555pub(crate) async fn email_staff(env: &Env, to: &str, subject: &str, lines: &[String]) -> Result<()> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily556 let link = "https://sudo.g1t.sh/costs";
557 let text = format!("{}\n\nCosts & margin: {link}\n\nSent by g1t-billing's margin guard (COSTS_ALERT_EMAIL).\n", lines.join("\n\n"));
558 let mut html = String::from("<div style=\"font-family:system-ui,sans-serif;max-width:560px;margin:0 auto;padding:24px 16px;color:#16150f\">");
559 for line in lines {
560 html.push_str(&format!("<p style=\"font-size:15px;line-height:1.6\">{}</p>", escape(line)));
561 }
562 html.push_str(&format!(
563 "<p><a href=\"{link}\">Open Costs &amp; margin in sudo</a></p><p style=\"font-size:13px;color:#6e6a5e\">Sent by g1t-billing's margin guard (COSTS_ALERT_EMAIL).</p></div>"
564 ));
565 let mail = Mail { to, from: "g1t <noreply@g1t.sh>", subject, text, html };
566 let binding = g1t_kit::js::binding(env, "EMAIL")?;
567 g1t_kit::js::call(&binding, "send", &[g1t_kit::js::to_js(&mail)?]).await?;
568 Ok(())
569}
570
571#[derive(Deserialize)]
572struct AlertRow {
573 id: String,
574 kind: String,
575 subject: String,
576 detail: String,
577 since: String,
578 opened_at: String,
579 emailed_at: Option<String>,
580}
581
582impl From<AlertRow> for MarginAlert {
583 fn from(r: AlertRow) -> Self {
584 MarginAlert { id: r.id, kind: r.kind, subject: r.subject, detail: r.detail, since: r.since, opened_at: r.opened_at, emailed_at: r.emailed_at }
585 }
586}
587
588#[derive(Deserialize)]
589struct MarginRow {
590 day: String,
591 bucket: String,
592 cf_cost_micros: i64,
593 own_cost_micros: i64,
594 value_micros: i64,
595 cash_micros: i64,
596 cf_quantity: f64,
597 own_quantity: f64,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it598 #[serde(default)]
599 given_micros: Option<i64>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily600}
601
602impl From<MarginRow> for ProductDay {
603 fn from(r: MarginRow) -> Self {
604 ProductDay {
605 day: r.day,
606 bucket: r.bucket,
607 cf_cost_micros: r.cf_cost_micros,
608 own_cost_micros: r.own_cost_micros,
609 value_micros: r.value_micros,
610 cash_micros: r.cash_micros,
611 cf_quantity: r.cf_quantity,
612 own_quantity: r.own_quantity,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it613 given_micros: r.given_micros.unwrap_or(0),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily614 }
615 }
616}
617
618impl Billing {
619 /// The day's work: read Cloudflare's bill and g1t's own counts,
620 /// reconcile, look for drift, measure unit costs, apply prices whose
621 /// day has come, and raise or clear alerts.
622 pub(crate) async fn costs_daily(&self, env: &Env, keeper: &crate::keeper::Keeper) -> Result<CostsRun> {
623 let mut run = CostsRun::default();
624 let (since, until) = match self.read_cloudflare(keeper, &mut run.problems).await? {
625 Some((since, until, lines)) => {
626 run.lines = lines;
627 (since, until)
628 }
629 // Without the bill, still reconcile what g1t knows itself, over
630 // the same days the bill would be read for.
631 None => {
632 #[derive(Deserialize)]
633 struct Last {
634 day: Option<String>,
635 }
636 let last = self.db.prepare("SELECT MAX(day) AS day FROM margin_days").first::<Last>(None).await?.and_then(|l| l.day);
637 costs::window(last.as_deref(), now_ms())
638 }
639 };
640 if let Err(error) = self.count_own(&since, &until).await {
641 run.problems.push(format!("g1t's own counts could not be read: {error}"));
642 }
643 self.snapshot_pending(&until).await?;
Models' margin read -14%: usage nothing paid for is valued at price, not $0644 // Reconciled over the whole window sudo shows, not only the days the
645 // bill was read for: it reads only what is already kept, so a change
646 // in how a day is valued reaches every day shown at the next run.
647 let window = day_before(&until, costs::BACKFILL_DAYS - 1);
648 let reconcile_from = if window < since { window } else { since.clone() };
649 run.days = self.reconcile_range(&reconcile_from, &until).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily650 let drift = self.find_drift(&until).await?;
651 run.proposals = self.measure_units(&until).await?;
652 self.apply_due_versions().await?;
653 run.alerts = self.raise_alerts(env, &until, &drift).await?;
654 if let Some(identity) = &self.identity
655 && let Err(error) = self.tell_owners_of_rises(identity).await
656 {
657 run.problems.push(format!("owners could not be told of a price rise: {error}"));
658 }
659 for problem in &run.problems {
660 worker::console_warn!("costs: {problem}");
661 }
662 Ok(run)
663 }
664
665 /// What each month-end source had come to by the end of `day`.
666 async fn snapshot_pending(&self, day: &str) -> Result<()> {
667 self.db
668 .prepare(
669 "INSERT INTO pending_days (day, workspace, source, cost_micros, charge_micros)
670 SELECT ?1, workspace, source, COALESCE(cost_micros, 0), COALESCE(charge_micros, 0) FROM pending_usage WHERE month = ?2
671 ON CONFLICT (day, workspace, source) DO UPDATE SET cost_micros = excluded.cost_micros, charge_micros = excluded.charge_micros",
672 )
673 .bind(&[day.into(), day[..7].into()])?
674 .run()
675 .await?;
676 Ok(())
677 }
678
679 /// What customers were charged on the days, by workspace and key.
680 async fn usage_rows(&self, since: &str, until: &str) -> Result<Vec<UsageRow>> {
681 #[derive(Deserialize)]
682 struct Row {
683 day: String,
684 workspace: String,
685 key: String,
686 internal: i64,
687 own_provider: i64,
688 cash: Option<i64>,
689 drawn: Option<i64>,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it690 credit: Option<i64>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily691 cost: Option<i64>,
692 }
693 let charged_here = crate::storage::CHARGED_HERE.iter().map(|s| format!("'{s}'")).collect::<Vec<_>>().join(", ");
694 let end = format!("{until}T23:59:59.999Z");
695 let rows = self
696 .db
697 .prepare(format!(
698 "SELECT substr(created_at, 1, 10) AS day, workspace,
699 CASE WHEN task = 'deployments' AND reference LIKE 'deploy/%' THEN 'builds' ELSE COALESCE(task, 'other') END AS key,
700 CASE WHEN workspace IN ({internal}) THEN 1 ELSE 0 END AS internal,
701 CASE WHEN billed_to = 'workspace' THEN 1 ELSE 0 END AS own_provider,
702 -SUM(amount_micros) AS cash,
703 SUM(COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0) + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0)) AS drawn,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it704 SUM(COALESCE(credit_micros, 0)) AS credit,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily705 SUM(COALESCE(cost_micros, 0)) AS cost
706 FROM ledger
707 WHERE kind = 'usage' AND created_at >= ?1 AND created_at <= ?2 AND COALESCE(task, '') NOT IN ({charged_here})
708 GROUP BY 1, 2, 3, 4, 5",
709 internal = crate::sales::INTERNAL_SQL
710 ))
711 .bind(&[since.into(), end.as_str().into()])?
712 .all()
713 .await?
714 .results::<Row>()?;
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it715 let mut internal = BTreeSet::new();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily716 let mut out: Vec<UsageRow> = rows
717 .into_iter()
718 .map(|r| {
719 // A workspace's own model provider was paid there: no cost
720 // to g1t. g1t's own workspaces are valued at price.
721 let cost = if r.own_provider == 1 { 0 } else { r.cost.unwrap_or(0) };
722 let cash = r.cash.unwrap_or(0);
Models' margin read -14%: usage nothing paid for is valued at price, not $0723 let paid = cash + r.drawn.unwrap_or(0);
724 let value = usage_value(r.internal == 1, cost, paid, self.margin_percent);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it725 let given = if r.internal == 1 { value } else { (value - cash - r.credit.unwrap_or(0)).max(0) };
726 if r.internal == 1 {
727 internal.insert(r.workspace.clone());
728 }
729 UsageRow { day: r.day, workspace: r.workspace, key: r.key, value, cash, cost, given }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily730 })
731 .collect();
732 // Month-end sources, from their daily snapshots.
733 #[derive(Deserialize)]
734 struct Snap {
735 day: String,
736 workspace: String,
737 source: String,
738 cost_micros: i64,
739 charge_micros: i64,
740 }
741 let snaps = self
742 .db
743 .prepare("SELECT day, workspace, source, cost_micros, charge_micros FROM pending_days WHERE day >= ?1 AND day <= ?2")
744 .bind(&[day_before(since, 1).into(), until.into()])?
745 .all()
746 .await?
747 .results::<Snap>()?
748 .into_iter()
749 .filter(|s| crate::storage::CHARGED_HERE.contains(&s.source.as_str()) || s.source == "domains")
750 .map(|s| (s.day, s.workspace, s.source, s.cost_micros, s.charge_micros))
751 .collect::<Vec<_>>();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it752 out.extend(pending_deltas(&snaps).into_iter().filter(|u| u.day.as_str() >= since).map(|mut u| {
753 if internal.contains(&u.workspace) {
754 u.given = u.value;
755 }
756 u
757 }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily758 // The plan's price, spread over the 30 days it pays for, so a month's
759 // payment does not read as one very good day and 29 bad ones.
760 #[derive(Deserialize)]
761 struct Plan {
762 day: String,
763 workspace: String,
764 micros: Option<i64>,
765 }
766 let plans = self
767 .db
768 .prepare(
769 "SELECT substr(paid_at, 1, 10) AS day, workspace, SUM(amount_micros) AS micros FROM plan_payments
770 WHERE paid_at >= ?1 AND paid_at <= ?2 GROUP BY 1, 2",
771 )
772 .bind(&[day_before(since, PLAN_DAYS - 1).into(), end.as_str().into()])?
773 .all()
774 .await?
775 .results::<Plan>()?;
776 for p in plans {
777 for (day, micros) in spread(&p.day, p.micros.unwrap_or(0), PLAN_DAYS) {
778 if day.as_str() >= since && day.as_str() <= until {
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it779 out.push(UsageRow { day, workspace: p.workspace.clone(), key: "plan".into(), value: micros, cash: micros, cost: 0, given: 0 });
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily780 }
781 }
782 }
783 Ok(out)
784 }
785
786 /// Reconciles the days and writes `margin_days` and `workspace_costs`.
787 async fn reconcile_range(&self, since: &str, until: &str) -> Result<u32> {
788 let rules = self.rules().await?;
789 #[derive(Deserialize)]
790 struct Map {
791 key: String,
792 bucket: String,
793 }
794 let revenue_map: BTreeMap<String, String> = self
795 .db
796 .prepare("SELECT key, bucket FROM revenue_map")
797 .all()
798 .await?
799 .results::<Map>()?
800 .into_iter()
801 .map(|m| (m.key, m.bucket))
802 .collect();
803 let lines = self
804 .db
805 .prepare("SELECT day, source, product, meter, quantity, cost_usd FROM cost_lines WHERE day >= ?1 AND day <= ?2")
806 .bind(&[since.into(), until.into()])?
807 .all()
808 .await?
809 .results::<LineRow>()?;
810 let own = self
811 .db
812 .prepare("SELECT day, meter, workspace, quantity FROM own_counts WHERE day >= ?1 AND day <= ?2")
813 .bind(&[since.into(), until.into()])?
814 .all()
815 .await?
816 .results::<OwnRow>()?;
817 let usage = self.usage_rows(since, until).await?;
818 let (days, workspaces) = fold(&rules, &revenue_map, &lines, &own, &usage);
819 let now = rfc3339(now_ms());
820 self.db
821 .batch(vec![
822 self.db.prepare("DELETE FROM margin_days WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
823 self.db.prepare("DELETE FROM workspace_costs WHERE day >= ?1 AND day <= ?2").bind(&[since.into(), until.into()])?,
824 ])
825 .await?;
826 for chunk in days.chunks(50) {
827 let mut statements = Vec::with_capacity(chunk.len());
828 for d in chunk {
829 statements.push(
830 self.db
831 .prepare(
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it832 "INSERT OR REPLACE INTO margin_days (day, bucket, cf_cost_micros, own_cost_micros, value_micros, cash_micros, cf_quantity, own_quantity, given_micros, computed_at)
833 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily834 )
835 .bind(&[
836 d.day.as_str().into(),
837 d.bucket.as_str().into(),
838 (d.cf_cost_micros as f64).into(),
839 (d.own_cost_micros as f64).into(),
840 (d.value_micros as f64).into(),
841 (d.cash_micros as f64).into(),
842 d.cf_quantity.into(),
843 d.own_quantity.into(),
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it844 (d.given_micros as f64).into(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily845 now.as_str().into(),
846 ])?,
847 );
848 }
849 self.db.batch(statements).await?;
850 }
851 for chunk in workspaces.chunks(50) {
852 let mut statements = Vec::with_capacity(chunk.len());
853 for w in chunk {
854 statements.push(
855 self.db
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it856 .prepare("INSERT OR REPLACE INTO workspace_costs (day, workspace, bucket, cost_micros, revenue_micros, value_micros, given_micros) VALUES (?, ?, ?, ?, ?, ?, ?)")
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily857 .bind(&[
858 w.day.as_str().into(),
859 w.workspace.as_str().into(),
860 w.bucket.as_str().into(),
861 (w.cost as f64).into(),
862 (w.revenue as f64).into(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead863 (w.value as f64).into(),
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it864 (w.given as f64).into(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily865 ])?,
866 );
867 }
868 self.db.batch(statements).await?;
869 }
870 Ok(costs::days_between(since, until).len() as u32)
871 }
872
873 async fn margin_days(&self, since: &str, until: &str) -> Result<Vec<ProductDay>> {
874 Ok(self
875 .db
876 .prepare("SELECT * FROM margin_days WHERE day >= ?1 AND day <= ?2 ORDER BY day, bucket")
877 .bind(&[since.into(), until.into()])?
878 .all()
879 .await?
880 .results::<MarginRow>()?
881 .into_iter()
882 .map(ProductDay::from)
883 .collect())
884 }
885
886 /// Drift over the last week, written to `cost_drift` (replacing the
887 /// last run's), with unmapped Cloudflare meters as leaks.
888 async fn find_drift(&self, until: &str) -> Result<Vec<(Drift, String)>> {
889 let since = day_before(until, DRIFT_DAYS - 1);
890 let settings = self.cost_settings().await?;
891 let rules = self.rules().await?;
892 let days = self.margin_days(&since, until).await?;
893 let mut by: BTreeMap<String, Vec<ProductDay>> = BTreeMap::new();
894 for d in days {
895 by.entry(d.bucket.clone()).or_default().push(d);
896 }
897 let mut found = Vec::new();
898 for (bucket, days) in &by {
899 let bucket_rules: Vec<&Rule> = rules.iter().filter(|r| &r.bucket == bucket).collect();
900 let threshold = bucket_rules.iter().map(|r| r.drift_percent).fold(f64::INFINITY, f64::min);
901 let threshold = if threshold.is_finite() { threshold } else { 10.0 };
902 let counted = bucket_rules.iter().any(|r| r.own_meter.is_some());
903 for drift in drifts(bucket, days, threshold, counted, settings.min_daily_cost_micros) {
904 let title = costs::bucket_title(bucket);
905 let detail = match drift.kind {
906 DriftKind::Count => format!(
One operation mapping, owned by repos; billing reads it instead of keeping its own907 "{title}: g1t counted {}, Cloudflare {} over the last {DRIFT_DAYS} days ({:+.1}%). Customers are charged for what g1t counts; check what Cloudflare counts as a unit and change the repos service's operation_mapping (set_operation_mapping).",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily908 crate::features::thousands(drift.ours.max(0.0).round() as u64),
909 crate::features::thousands(drift.cloudflare.max(0.0).round() as u64),
910 drift.delta_percent.unwrap_or(0.0)
911 ),
912 DriftKind::Cost => format!(
913 "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days; the price book's cost of the same usage is {} ({:+.1}%). A price may be stale: see the proposals.",
914 dollars(drift.cloudflare as i64),
915 dollars(drift.ours as i64),
916 drift.delta_percent.unwrap_or(0.0)
917 ),
918 DriftKind::Leak if bucket == UNMAPPED => {
919 format!("Cloudflare charged {} for meters no mapping claims. Map them on Costs & margin.", dollars(drift.cloudflare as i64))
920 }
921 DriftKind::Leak => format!(
922 "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days and customers were charged nothing for it.",
923 dollars(drift.cloudflare as i64)
924 ),
925 };
926 found.push((drift, detail));
927 }
928 }
929 let now = rfc3339(now_ms());
930 let mut statements = vec![self.db.prepare("DELETE FROM cost_drift")];
931 for (drift, detail) in &found {
932 statements.push(
933 self.db
934 .prepare("INSERT OR REPLACE INTO cost_drift (bucket, kind, ours, cloudflare, delta_percent, detail, found_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
935 .bind(&[
936 drift.bucket.as_str().into(),
937 drift.kind.as_str().into(),
938 drift.ours.into(),
939 drift.cloudflare.into(),
940 drift.delta_percent.map_or(JsValue::NULL, JsValue::from),
941 detail.as_str().into(),
942 now.as_str().into(),
943 ])?,
944 );
945 }
946 self.db.batch(statements).await?;
947 Ok(found)
948 }
949
950 /// Unit costs from the bill for mappings that scale to g1t's own count
951 /// (git operations), proposed to the price book.
952 async fn measure_units(&self, until: &str) -> Result<u32> {
953 #[derive(Deserialize)]
954 struct Scaled {
955 product: String,
956 meter: String,
957 price_meter: String,
958 own_meter: String,
959 unit: Option<String>,
960 }
961 let scaled = self
962 .db
963 .prepare(
964 "SELECT m.product, m.meter, m.price_meter, m.own_meter, p.unit FROM cost_map m LEFT JOIN prices p ON p.meter = m.price_meter
965 WHERE m.scale_to_own = 1 AND m.price_meter IS NOT NULL AND m.own_meter IS NOT NULL",
966 )
967 .all()
968 .await?
969 .results::<Scaled>()?;
970 let since = day_before(until, MEASURE_DAYS - 1);
971 let rules = self.rules().await?;
972 let mut proposed = 0;
973 for s in scaled {
974 #[derive(Deserialize)]
975 struct Day {
976 product: String,
977 meter: String,
978 quantity: f64,
979 cost_usd: f64,
980 }
981 let lines = self
982 .db
983 .prepare("SELECT product, meter, quantity, cost_usd FROM cost_lines WHERE source = ?1 AND product = ?2 AND day >= ?3 AND day <= ?4")
984 .bind(&[SOURCE_BILLABLE.into(), s.product.as_str().into(), since.as_str().into(), until.into()])?
985 .all()
986 .await?
987 .results::<Day>()?;
988 // Only the lines this very mapping claims.
989 let mine: Vec<(f64, f64)> = lines
990 .iter()
991 .filter(|l| costs::classify(&rules, &l.product, &l.meter).is_some_and(|r| r.product == s.product && r.meter == s.meter))
992 .map(|l| (l.quantity, l.cost_usd))
993 .collect();
994 let Some(rate) = billed_rate(&mine) else { continue };
995 let cf_units: f64 = mine.iter().map(|(q, _)| q).sum();
996 #[derive(Deserialize)]
997 struct Own {
998 total: Option<f64>,
999 }
1000 let own_units = self
1001 .db
1002 .prepare("SELECT SUM(quantity) AS total FROM own_counts WHERE meter = ?1 AND day >= ?2 AND day <= ?3")
1003 .bind(&[s.own_meter.as_str().into(), since.as_str().into(), until.into()])?
1004 .first::<Own>(None)
1005 .await?
1006 .and_then(|o| o.total)
1007 .unwrap_or(0.0);
1008 let Some(per_unit) = derived_unit_cost(rate, cf_units, own_units) else { continue };
1009 let size = unit_size(s.unit.as_deref().unwrap_or("1"));
1010 let measured = per_unit * size * 1_000_000.0;
1011 let reason = format!(
1012 "Cloudflare billed ${:.4} per 1,000 of its units and counted {:.2} of them for each one g1t counted over the last {MEASURE_DAYS} days ({} against {})",
1013 rate * 1000.0,
1014 cf_units / own_units,
1015 crate::features::thousands(cf_units.round() as u64),
1016 crate::features::thousands(own_units.round() as u64)
1017 );
1018 if self.propose(&s.price_meter, measured, &reason, "reconciler").await?.is_some() {
1019 proposed += 1;
1020 }
1021 }
1022 Ok(proposed)
1023 }
1024
1025 /// Opens, updates and closes margin alerts, and emails staff about new
1026 /// ones (and open ones each week).
1027 async fn raise_alerts(&self, env: &Env, until: &str, drift: &[(Drift, String)]) -> Result<u32> {
1028 let settings = self.cost_settings().await?;
1029 let since = day_before(until, u64::from(settings.alert_days.max(1)) - 1);
1030 let days = self.margin_days(&since, until).await?;
1031 let mut conditions: Vec<(String, String, String, String)> = Vec::new();
1032 // Each product under the floor.
1033 let mut by: BTreeMap<String, Vec<(String, i64, i64)>> = BTreeMap::new();
1034 let mut all: BTreeMap<String, (i64, i64)> = BTreeMap::new();
1035 for d in &days {
1036 let overall = all.entry(d.day.clone()).or_default();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1037 // What g1t gave away (comped workspaces, free periods, the
1038 // trial and the pools) is a budget it chose to spend, watched on
1039 // its own (budget.rs): not part of whether what is sold pays.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1040 overall.0 += d.cash_micros;
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1041 overall.1 += (d.cost() - d.given_micros).max(0);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1042 if !OVERHEAD.contains(&d.bucket.as_str()) && d.bucket != UNMAPPED {
1043 by.entry(d.bucket.clone()).or_default().push((d.day.clone(), d.value_micros, d.cost()));
1044 }
1045 }
1046 let floor = settings.margin_floor_percent;
1047 let n = settings.alert_days as usize;
1048 for (bucket, series) in &by {
1049 if let Some((from, worst)) = breach(series, floor, n, settings.min_daily_cost_micros) {
1050 conditions.push((
1051 "margin".into(),
1052 bucket.clone(),
1053 format!("{}: margin under {floor:.0}% for {n} days running, as low as {worst:.1}%.", costs::bucket_title(bucket)),
1054 from,
1055 ));
Margin alerts measure what is sold, and say dollars when a percentage would mislead1056 }
1057 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1058 let series: Vec<(String, i64, i64)> = all.into_iter().map(|(day, (revenue, cost))| (day, revenue, cost)).collect();
1059 if let Some((from, worst)) = breach(&series, floor, n, settings.min_daily_cost_micros) {
Margin alerts measure what is sold, and say dollars when a percentage would mislead1060 let tail = &series[series.len().saturating_sub(n)..];
1061 let (took, spent) = tail.iter().fold((0i64, 0i64), |(r, c), (_, revenue, cost)| (r + revenue, c + cost));
1062 conditions.push(("overall".into(), "g1t".into(), overall_detail(took, spent, n, floor, worst), from));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1063 }
1064 for (d, detail) in drift {
1065 let kind = if d.kind == DriftKind::Leak { "leak" } else { "drift" };
1066 conditions.push((kind.into(), format!("{}:{}", d.bucket, d.kind.as_str()), detail.clone(), until.to_owned()));
1067 }
1068 // Workspaces costing more than they pay.
1069 for (workspace, cost, revenue) in self.workspace_anomalies(until, &settings).await? {
1070 conditions.push((
1071 "workspace".into(),
1072 workspace.clone(),
Margin alerts measure what is sold, and say dollars when a percentage would mislead1073 format!(
1074 "{workspace} cost g1t {} on Cloudflare over {ANOMALY_DAYS} days, and its usage was priced at {}: its prices are below cost.",
1075 dollars(cost),
1076 dollars(revenue)
1077 ),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1078 day_before(until, ANOMALY_DAYS - 1),
1079 ));
1080 }
1081
1082 let open = self
1083 .db
1084 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL")
1085 .all()
1086 .await?
1087 .results::<AlertRow>()?;
1088 let now = now_ms();
1089 let stamp = rfc3339(now);
1090 let mut to_email: Vec<String> = Vec::new();
1091 let mut kept: BTreeSet<String> = BTreeSet::new();
1092 for (kind, subject, detail, from) in &conditions {
1093 match open.iter().find(|a| &a.kind == kind && &a.subject == subject) {
1094 Some(alert) => {
1095 kept.insert(alert.id.clone());
1096 self.db
1097 .prepare("UPDATE margin_alerts SET detail = ? WHERE id = ?")
1098 .bind(&[detail.as_str().into(), alert.id.as_str().into()])?
1099 .run()
1100 .await?;
1101 let stale = alert
1102 .emailed_at
1103 .as_deref()
1104 .and_then(g1t_contracts::time::parse_rfc3339)
1105 .is_none_or(|at| now.saturating_sub(at) >= REMIND_MS);
1106 if stale && kind != "workspace" {
1107 to_email.push(format!("Still open: {detail}"));
1108 kept.insert(format!("email:{}", alert.id));
1109 }
1110 }
1111 None => {
1112 let id = new_id("mal", now);
1113 self.db
1114 .prepare("INSERT INTO margin_alerts (id, kind, subject, detail, since, opened_at) VALUES (?, ?, ?, ?, ?, ?)")
1115 .bind(&[id.as_str().into(), kind.as_str().into(), subject.as_str().into(), detail.as_str().into(), from.as_str().into(), stamp.as_str().into()])?
1116 .run()
1117 .await?;
1118 kept.insert(id.clone());
Margin alerts measure what is sold, and say dollars when a percentage would mislead1119 // A workspace's is for Reach out, not the inbox.
1120 if kind != "workspace" {
1121 to_email.push(detail.clone());
1122 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1123 kept.insert(format!("email:{id}"));
1124 }
1125 }
1126 }
1127 for alert in &open {
1128 if !kept.contains(&alert.id) {
1129 self.db
1130 .prepare("UPDATE margin_alerts SET resolved_at = ? WHERE id = ?")
1131 .bind(&[stamp.as_str().into(), alert.id.as_str().into()])?
1132 .run()
1133 .await?;
1134 }
1135 }
1136 let to = env.var("COSTS_ALERT_EMAIL").map(|v| v.to_string()).unwrap_or_default();
1137 if !to_email.is_empty() && !to.trim().is_empty() {
1138 let subject = format!("[g1t costs] {} margin alert{}", to_email.len(), if to_email.len() == 1 { "" } else { "s" });
1139 match email_staff(env, to.trim(), &subject, &to_email).await {
1140 Ok(()) => {
1141 for marker in kept.iter().filter_map(|k| k.strip_prefix("email:")) {
1142 self.db
1143 .prepare("UPDATE margin_alerts SET emailed_at = ? WHERE id = ?")
1144 .bind(&[stamp.as_str().into(), marker.into()])?
1145 .run()
1146 .await?;
1147 }
1148 }
1149 Err(error) => worker::console_error!("could not email the margin alerts: {error}"),
1150 }
1151 }
1152 Ok(conditions.len() as u32)
1153 }
1154
1155 /// Workspaces costing g1t more than they pay over 30 days, not g1t's own.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1156 /// Each day's cost shared out to comped workspaces.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1157 async fn workspace_anomalies(&self, until: &str, settings: &CostSettings) -> Result<Vec<(String, i64, i64)>> {
1158 #[derive(Deserialize)]
1159 struct Row {
1160 workspace: String,
1161 cost: Option<i64>,
1162 revenue: Option<i64>,
1163 }
1164 let rows = self
1165 .db
1166 .prepare(format!(
Margin alerts measure what is sold, and say dollars when a percentage would mislead1167 // Against what its usage was priced at, not the cash it
1168 // paid: a trial or a gift paying for usage is not a price
1169 // below cost.
The workspace cost alert compares only days that carry their value, not the days before it was kept1170 // Days from before value_micros was kept have none: only days
1171 // since the first one that does are compared.
Margin alerts measure what is sold, and say dollars when a percentage would mislead1172 "SELECT workspace, SUM(cost_micros) AS cost, SUM(value_micros) AS revenue FROM workspace_costs
The workspace cost alert compares only days that carry their value, not the days before it was kept1173 WHERE day >= ?1 AND day <= ?2 AND workspace NOT IN ({})
1174 AND day >= (SELECT MIN(day) FROM workspace_costs WHERE value_micros > 0)
1175 GROUP BY workspace",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1176 crate::sales::INTERNAL_SQL
1177 ))
1178 .bind(&[day_before(until, ANOMALY_DAYS - 1).into(), until.into()])?
1179 .all()
1180 .await?
1181 .results::<Row>()?;
1182 let rows: Vec<(String, i64, i64)> = rows.into_iter().map(|r| (r.workspace, r.cost.unwrap_or(0), r.revenue.unwrap_or(0))).collect();
1183 Ok(anomalies(&rows, settings.anomaly_factor, settings.anomaly_floor_micros))
1184 }
1185
1186 /// For Reach out: workspaces with an open cost-over-revenue alert,
1187 /// each with its detail and cost.
1188 pub(crate) async fn costing_more_than_they_pay(&self) -> Result<Vec<(String, String, i64)>> {
1189 let alerts = self
1190 .db
1191 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL AND kind = 'workspace' ORDER BY opened_at DESC LIMIT 50")
1192 .all()
1193 .await?
1194 .results::<AlertRow>()?;
1195 let mut out = Vec::new();
1196 for alert in alerts {
1197 #[derive(Deserialize)]
1198 struct Cost {
1199 cost: Option<i64>,
1200 }
1201 let cost = self
1202 .db
1203 .prepare("SELECT SUM(cost_micros) AS cost FROM workspace_costs WHERE workspace = ? AND day >= ?")
1204 .bind(&[alert.subject.as_str().into(), alert.since.as_str().into()])?
1205 .first::<Cost>(None)
1206 .await?
1207 .and_then(|c| c.cost)
1208 .unwrap_or(0);
1209 out.push((alert.subject, alert.detail, cost));
1210 }
1211 Ok(out)
1212 }
1213
1214 /// `admin_cost_alerts`: what sudo's banner says.
1215 pub(crate) async fn admin_cost_alerts(&self, _: AdminCostAlertsArgs) -> Result<Vec<MarginAlert>> {
1216 Ok(self
1217 .db
1218 .prepare("SELECT * FROM margin_alerts WHERE resolved_at IS NULL ORDER BY opened_at DESC LIMIT 50")
1219 .all()
1220 .await?
1221 .results::<AlertRow>()?
1222 .into_iter()
1223 .map(MarginAlert::from)
1224 .collect())
1225 }
1226
1227 /// `admin_run_costs`: the daily run, now.
1228 pub(crate) async fn admin_run_costs(&self, env: &Env, a: AdminRunCostsArgs) -> Result<Outcome<CostsRun>> {
1229 let keeper = crate::keeper::Keeper::from_env(env);
1230 let run = self.costs_daily(env, &keeper).await?;
1231 if !a.by.is_empty() {
1232 self.audit(
1233 "costs",
1234 "costs_run",
1235 &format!("{} lines, {} days, {} proposals, {} alerts", run.lines, run.days, run.proposals, run.alerts),
1236 &a.by,
1237 )
1238 .await?;
1239 }
1240 Ok(Outcome::Ok(run))
1241 }
1242
1243 /// `admin_set_cost_mapping`.
1244 pub(crate) async fn admin_set_cost_mapping(&self, a: AdminSetCostMappingArgs) -> Result<Outcome<CostMapping>> {
1245 let product = costs::slug(&a.product);
1246 let meter = if a.meter.trim() == "*" { "*".to_owned() } else { costs::slug(&a.meter) };
1247 if product.is_empty() || meter.is_empty() {
1248 return Ok(Outcome::fail(FailureCode::Invalid, "Name Cloudflare's product and a meter (or * for all of it)."));
1249 }
1250 let now = rfc3339(now_ms());
1251 if a.remove {
1252 self.db
1253 .prepare("DELETE FROM cost_map WHERE product = ? AND meter = ?")
1254 .bind(&[product.as_str().into(), meter.as_str().into()])?
1255 .run()
1256 .await?;
1257 self.audit("costs", "cost_mapping_removed", &format!("{product}/{meter}"), &a.by).await?;
1258 return Ok(Outcome::Ok(CostMapping {
1259 product,
1260 meter,
1261 bucket: String::new(),
1262 price_meter: None,
1263 own_meter: None,
1264 scale_to_own: false,
1265 drift_percent: 0.0,
1266 note: String::new(),
1267 updated_at: now,
1268 updated_by: a.by,
1269 }));
1270 }
1271 let bucket = costs::slug(&a.bucket);
1272 if bucket.is_empty() {
1273 return Ok(Outcome::fail(FailureCode::Invalid, "Say which of g1t's products it is a cost of."));
1274 }
1275 let clean = |v: Option<String>| v.map(|v| v.trim().to_owned()).filter(|v| !v.is_empty());
1276 let (price_meter, own_meter) = (clean(a.price_meter), clean(a.own_meter));
1277 let drift = a.drift_percent.filter(|d| d.is_finite() && *d > 0.0).unwrap_or(10.0);
1278 self.db
1279 .prepare(
1280 "INSERT INTO cost_map (product, meter, bucket, price_meter, own_meter, scale_to_own, drift_percent, note, updated_at, updated_by)
1281 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
1282 ON CONFLICT (product, meter) DO UPDATE SET bucket = ?3, price_meter = ?4, own_meter = ?5, scale_to_own = ?6,
1283 drift_percent = ?7, note = ?8, updated_at = ?9, updated_by = ?10",
1284 )
1285 .bind(&[
1286 product.as_str().into(),
1287 meter.as_str().into(),
1288 bucket.as_str().into(),
1289 crate::optional(price_meter.as_deref()),
1290 crate::optional(own_meter.as_deref()),
1291 i32::from(a.scale_to_own).into(),
1292 drift.into(),
1293 a.note.trim().into(),
1294 now.as_str().into(),
1295 a.by.as_str().into(),
1296 ])?
1297 .run()
1298 .await?;
1299 self.audit("costs", "cost_mapping", &format!("{product}/{meter} → {bucket}"), &a.by).await?;
1300 Ok(Outcome::Ok(CostMapping {
1301 product,
1302 meter,
1303 bucket,
1304 price_meter,
1305 own_meter,
1306 scale_to_own: a.scale_to_own,
1307 drift_percent: drift,
1308 note: a.note.trim().to_owned(),
1309 updated_at: now,
1310 updated_by: a.by,
1311 }))
1312 }
1313
1314 /// `admin_costs`: the Costs & margin page.
1315 pub(crate) async fn admin_costs(&self, a: AdminCostsArgs, configured: bool) -> Result<CostsReport> {
1316 let until = rfc3339(now_ms())[..10].to_owned();
1317 let span = u64::from(a.days.unwrap_or(30).clamp(7, 90));
1318 let since = day_before(&until, span - 1);
1319 let days = self.margin_days(&since, &until).await?;
1320 let rules = self.rules().await?;
1321
1322 let mut products: BTreeMap<String, ProductMargin> = BTreeMap::new();
1323 let mut overall = OverallMargin::default();
1324 for d in &days {
1325 let p = products.entry(d.bucket.clone()).or_insert_with(|| ProductMargin {
1326 bucket: d.bucket.clone(),
1327 title: costs::bucket_title(&d.bucket),
1328 cost_source: if NOT_CLOUDFLARE.contains(&d.bucket.as_str()) { "ledger" } else { "cloudflare" }.into(),
1329 overhead: OVERHEAD.contains(&d.bucket.as_str()),
1330 ..ProductMargin::default()
1331 });
1332 p.cf_cost_micros += d.cf_cost_micros;
1333 p.own_cost_micros += d.own_cost_micros;
1334 p.value_micros += d.value_micros;
1335 p.cost_micros += d.cost();
1336 overall.cost_micros += d.cost();
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1337 overall.given_micros += d.given_micros;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1338 if d.bucket == "platform" {
1339 overall.plans_micros += d.cash_micros;
1340 } else {
1341 overall.usage_micros += d.cash_micros;
1342 }
1343 }
1344 for p in products.values_mut() {
1345 p.margin_micros = p.value_micros - p.cost_micros;
1346 p.margin_percent = margin_percent(p.value_micros, p.cost_micros);
1347 }
1348 let revenue = overall.usage_micros + overall.plans_micros;
1349 overall.margin_micros = revenue - overall.cost_micros;
1350 overall.margin_percent = margin_percent(revenue, overall.cost_micros);
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1351 let sold = (overall.cost_micros - overall.given_micros).max(0);
1352 overall.sold_margin_micros = revenue - sold;
1353 overall.sold_margin_percent = margin_percent(revenue, sold);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1354 let mut products: Vec<ProductMargin> = products.into_values().collect();
1355 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
1356
1357 #[derive(Deserialize)]
1358 struct DriftRow {
1359 bucket: String,
1360 kind: String,
1361 ours: f64,
1362 cloudflare: f64,
1363 delta_percent: Option<f64>,
1364 detail: String,
1365 found_at: String,
1366 }
1367 let drift = self
1368 .db
1369 .prepare("SELECT * FROM cost_drift ORDER BY kind, bucket")
1370 .all()
1371 .await?
1372 .results::<DriftRow>()?
1373 .into_iter()
1374 .map(|r| CostDrift {
1375 title: costs::bucket_title(&r.bucket),
1376 bucket: r.bucket,
1377 kind: r.kind,
1378 ours: r.ours,
1379 cloudflare: r.cloudflare,
1380 delta_percent: r.delta_percent,
1381 detail: r.detail,
1382 found_at: r.found_at,
1383 })
1384 .collect();
1385
1386 #[derive(Deserialize)]
1387 struct Top {
1388 workspace: String,
1389 cost: Option<i64>,
1390 revenue: Option<i64>,
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1391 given: Option<i64>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1392 internal: i64,
1393 }
1394 let top_workspaces = self
1395 .db
1396 .prepare(format!(
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1397 "SELECT workspace, SUM(cost_micros) AS cost, SUM(revenue_micros) AS revenue, SUM(given_micros) AS given,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1398 CASE WHEN workspace IN ({}) THEN 1 ELSE 0 END AS internal
1399 FROM workspace_costs WHERE day >= ?1 AND day <= ?2 GROUP BY workspace ORDER BY cost DESC LIMIT 15",
1400 crate::sales::INTERNAL_SQL
1401 ))
1402 .bind(&[since.as_str().into(), until.as_str().into()])?
1403 .all()
1404 .await?
1405 .results::<Top>()?
1406 .into_iter()
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1407 .map(|t| WorkspaceCost { workspace: t.workspace, cost_micros: t.cost.unwrap_or(0), revenue_micros: t.revenue.unwrap_or(0), given_micros: t.given.unwrap_or(0), internal: t.internal == 1 })
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1408 .collect();
1409
1410 #[derive(Deserialize)]
1411 struct Summary {
1412 source: String,
1413 product: String,
1414 meter: String,
1415 raw_name: String,
1416 unit: String,
1417 quantity: f64,
1418 cost_usd: f64,
1419 }
1420 let lines = self
1421 .db
1422 .prepare(
1423 "SELECT source, product, meter, MAX(raw_name) AS raw_name, MAX(unit) AS unit, SUM(quantity) AS quantity, SUM(cost_usd) AS cost_usd
1424 FROM cost_lines WHERE day >= ?1 AND day <= ?2 GROUP BY source, product, meter ORDER BY cost_usd DESC, product, meter LIMIT 200",
1425 )
1426 .bind(&[since.as_str().into(), until.as_str().into()])?
1427 .all()
1428 .await?
1429 .results::<Summary>()?
1430 .into_iter()
1431 .map(|l| CostLineSummary {
1432 bucket: costs::classify(&rules, &l.product, &l.meter).map(|r| r.bucket.clone()),
1433 product: l.product,
1434 meter: l.meter,
1435 raw_name: l.raw_name,
1436 unit: l.unit,
1437 source: l.source,
1438 quantity: l.quantity,
1439 cost_micros: micros(l.cost_usd),
1440 })
1441 .collect();
1442
1443 #[derive(Deserialize)]
1444 struct MapRow {
1445 product: String,
1446 meter: String,
1447 bucket: String,
1448 price_meter: Option<String>,
1449 own_meter: Option<String>,
1450 scale_to_own: i64,
1451 drift_percent: f64,
1452 note: String,
1453 updated_at: String,
1454 updated_by: String,
1455 }
1456 let mappings = self
1457 .db
1458 .prepare("SELECT * FROM cost_map ORDER BY product, meter")
1459 .all()
1460 .await?
1461 .results::<MapRow>()?
1462 .into_iter()
1463 .map(|m| CostMapping {
1464 product: m.product,
1465 meter: m.meter,
1466 bucket: m.bucket,
1467 price_meter: m.price_meter,
1468 own_meter: m.own_meter,
1469 scale_to_own: m.scale_to_own == 1,
1470 drift_percent: m.drift_percent,
1471 note: m.note,
1472 updated_at: m.updated_at,
1473 updated_by: m.updated_by,
1474 })
1475 .collect();
1476
1477 #[derive(Deserialize)]
1478 struct Fetched {
1479 at: Option<String>,
1480 }
1481 let fetched_at = self.db.prepare("SELECT MAX(fetched_at) AS at FROM cost_lines").first::<Fetched>(None).await?.and_then(|f| f.at);
1482
1483 Ok(CostsReport {
1484 configured,
1485 fetched_at,
1486 days: days
1487 .iter()
1488 .map(|d| CostDay {
1489 day: d.day.clone(),
1490 bucket: d.bucket.clone(),
1491 cf_cost_micros: d.cf_cost_micros,
1492 own_cost_micros: d.own_cost_micros,
1493 value_micros: d.value_micros,
1494 cash_micros: d.cash_micros,
1495 })
1496 .collect(),
1497 since,
1498 until,
1499 products,
1500 overall,
1501 drift,
1502 alerts: self.admin_cost_alerts(AdminCostAlertsArgs {}).await?,
1503 proposals: self.proposals().await?,
1504 versions: self.versions().await?,
1505 top_workspaces,
1506 lines,
1507 mappings,
1508 settings: self.cost_settings().await?,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1509 caps: self.spend_caps().await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1510 })
1511 }
1512}
1513
1514#[cfg(test)]
1515mod tests {
1516 use super::*;
1517
Margin alerts measure what is sold, and say dollars when a percentage would mislead1518 #[test]
Models' margin read -14%: usage nothing paid for is valued at price, not $01519 fn usage_nothing_paid_for_is_valued_at_price_and_paid_usage_at_what_was_paid() {
1520 // A free period: charged nothing, drawn from nothing.
1521 assert_eq!(usage_value(false, 1_000_000, 0, 20), 1_200_000);
1522 // Charged, or drawn from a trial: what was paid.
1523 assert_eq!(usage_value(false, 1_000_000, 1_200_000, 20), 1_200_000);
1524 assert_eq!(usage_value(false, 1_000_000, 900_000, 20), 900_000);
1525 // g1t's own: at price.
1526 assert_eq!(usage_value(true, 1_000_000, 0, 20), 1_200_000);
1527 // No cost, nothing paid: nothing.
1528 assert_eq!(usage_value(false, 0, 0, 20), 0);
1529 }
1530
1531 #[test]
Margin alerts measure what is sold, and say dollars when a percentage would mislead1532 fn the_overall_alert_says_dollars_while_little_comes_in() {
1533 let small = overall_detail(90_000, 7_500_000, 3, 10.0, -8239.7);
1534 assert!(small.contains("took in $0.09 against $7.50"), "{small}");
1535 assert!(!small.contains('%'), "{small}");
1536 let real = overall_detail(30_000_000, 40_000_000, 3, 10.0, -33.3);
1537 assert!(real.contains("as low as -33.3%"), "{real}");
1538 }
1539
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1540 fn rule(product: &str, meter: &str, bucket: &str, own: Option<&str>) -> Rule {
1541 Rule { product: product.into(), meter: meter.into(), bucket: bucket.into(), price_meter: None, own_meter: own.map(Into::into), drift_percent: 10.0 }
1542 }
1543
1544 fn rules() -> Vec<Rule> {
1545 vec![
1546 rule("containers", "*", "sandboxes", None),
1547 rule("workers", "*", "platform", None),
1548 rule("artifacts", "*", "git", Some("git_operations")),
1549 rule("artifacts", "events_", "git", Some("git_operations")),
1550 ]
1551 }
1552
1553 fn revenue_map() -> BTreeMap<String, String> {
1554 [("sandbox", "sandboxes"), ("git", "git"), ("plan", "platform")].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
1555 }
1556
1557 fn line(day: &str, source: &str, product: &str, meter: &str, quantity: f64, cost: f64) -> LineRow {
1558 LineRow { day: day.into(), source: source.into(), product: product.into(), meter: meter.into(), quantity, cost_usd: cost }
1559 }
1560
1561 fn usage(day: &str, workspace: &str, key: &str, value: i64, cash: i64, cost: i64) -> UsageRow {
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1562 UsageRow { day: day.into(), workspace: workspace.into(), key: key.into(), value, cash, cost, given: 0 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1563 }
1564
1565 #[test]
1566 fn a_day_puts_the_bill_g1ts_counts_and_charges_side_by_side() {
1567 let lines = vec![
1568 line("2026-10-15", SOURCE_BILLABLE, "containers", "container_memory", 1000.0, 2.00),
1569 line("2026-10-15", SOURCE_BILLABLE, "artifacts", "artifacts_operations", 30_000.0, 3.00),
1570 // Artifacts' own events: not used while the bill has a count.
1571 line("2026-10-15", SOURCE_ARTIFACTS, "artifacts", "events_pull", 29_000.0, 0.0),
1572 line("2026-10-15", SOURCE_BILLABLE, "workers", "workers_cpu_ms", 1.0, 0.50),
1573 line("2026-10-15", SOURCE_BILLABLE, "browser_rendering", "browser_hours", 2.0, 0.25),
1574 ];
1575 let own = vec![
1576 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "acme".into(), quantity: 7_500.0 },
1577 OwnRow { day: "2026-10-15".into(), meter: "git_operations".into(), workspace: "beta".into(), quantity: 2_500.0 },
1578 ];
1579 let usage = vec![
1580 usage("2026-10-15", "acme", "sandbox", 2_400_000, 1_000_000, 2_000_000),
1581 usage("2026-10-15", "beta", "sandbox", 1_200_000, 1_200_000, 1_000_000),
1582 usage("2026-10-15", "acme", "git", 600_000, 600_000, 500_000),
1583 usage("2026-10-15", "acme", "implement", 120_000, 120_000, 100_000),
1584 usage("2026-10-15", "beta", "plan", 20_000_000, 20_000_000, 0),
1585 ];
1586 let (days, workspaces) = fold(&rules(), &revenue_map(), &lines, &own, &usage);
1587 let get = |bucket: &str| days.iter().find(|d| d.bucket == bucket).unwrap();
1588 let sandboxes = get("sandboxes");
1589 assert_eq!((sandboxes.cf_cost_micros, sandboxes.own_cost_micros, sandboxes.value_micros, sandboxes.cash_micros), (2_000_000, 3_000_000, 3_600_000, 2_200_000));
1590 let git = get("git");
1591 assert_eq!(git.cf_cost_micros, 3_000_000);
1592 assert_eq!((git.cf_quantity, git.own_quantity), (30_000.0, 10_000.0));
1593 assert_eq!(get("platform").value_micros, 20_000_000);
1594 // Not mapped: a leak until someone maps it.
1595 assert_eq!(get(UNMAPPED).cf_cost_micros, 250_000);
1596 // Models: no Cloudflare line, their cost is g1t's own.
1597 assert_eq!(get("models").cost(), 100_000);
1598 // Git's cost shared by g1t's own counts (Cloudflare gave none per
1599 // workspace here): three quarters to acme.
1600 let share = |ws: &str, bucket: &str| workspaces.iter().find(|w| w.workspace == ws && w.bucket == bucket).map(|w| (w.cost, w.revenue));
1601 assert_eq!(share("acme", "git"), Some((2_250_000, 600_000)));
1602 assert_eq!(share("beta", "git"), Some((750_000, 0)));
1603 // Every bucket's cost is shared out exactly.
1604 for d in &days {
1605 let shared: i64 = workspaces.iter().filter(|w| w.bucket == d.bucket).map(|w| w.cost).sum();
1606 assert_eq!(shared, d.cost(), "{}", d.bucket);
1607 }
1608 }
1609
1610 #[test]
1611 fn artifacts_events_count_when_the_bill_does_not() {
1612 let lines = vec![
1613 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_pull", 120.0, 0.0),
1614 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_push", 30.0, 0.0),
1615 line("2026-10-05", SOURCE_ARTIFACTS, "artifacts", "events_ratelimited", 9.0, 0.0),
1616 ];
1617 let (days, _) = fold(&rules(), &revenue_map(), &lines, &[], &[]);
1618 assert_eq!(days[0].cf_quantity, 150.0);
1619 assert_eq!(days[0].cf_cost_micros, 0);
1620 }
1621
1622 #[test]
1623 fn month_end_meters_are_told_by_the_day_from_snapshots() {
1624 let snap = |day: &str, cost: i64, charge: i64| (day.to_string(), "acme".to_string(), "git".to_string(), cost, charge);
1625 let rows = pending_deltas(&[snap("2026-10-30", 100, 120), snap("2026-10-31", 250, 300), snap("2026-11-01", 40, 48), snap("2026-11-02", 40, 48)]);
1626 assert_eq!(
1627 rows.iter().map(|r| (r.day.as_str(), r.cost, r.value)).collect::<Vec<_>>(),
1628 vec![("2026-10-30", 100, 120), ("2026-10-31", 150, 180), ("2026-11-01", 40, 48)]
1629 );
1630 }
1631
1632 #[test]
1633 fn a_plan_payment_is_spread_over_the_month_it_pays_for() {
1634 let days = spread("2026-10-01T00:00:00.000Z", 20_000_000, 30);
1635 assert_eq!(days.len(), 30);
1636 assert_eq!(days[0], ("2026-10-01".to_string(), 666_667));
1637 assert_eq!(days[29], ("2026-10-30".to_string(), 666_666));
1638 assert_eq!(days.iter().map(|d| d.1).sum::<i64>(), 20_000_000);
1639 assert!(spread("2026-10-01", 0, 30).is_empty());
1640 assert_eq!(dollars(17_024_000), "$17.02");
1641 assert_eq!(dollars(-27_668_620), "-$27.67");
1642 assert_eq!(dollars(63_000), "$0.063");
1643 }
1644
1645 #[test]
1646 fn margins_and_deltas() {
1647 assert_eq!(margin_percent(1_200_000, 1_000_000).map(|m| (m * 100.0).round() / 100.0), Some(16.67));
1648 assert_eq!(margin_percent(0, 5), None);
1649 assert_eq!(delta_percent(110.0, 100.0), Some(10.0));
1650 assert_eq!(delta_percent(1.0, 0.0), None);
1651 }
1652
1653 fn day(bucket: &str, cf: i64, own: i64, value: i64, cfq: f64, ownq: f64) -> ProductDay {
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1654 ProductDay { day: "2026-10-15".into(), bucket: bucket.into(), cf_cost_micros: cf, own_cost_micros: own, value_micros: value, cash_micros: value, cf_quantity: cfq, own_quantity: ownq, given_micros: 0 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1655 }
1656
1657 #[test]
1658 fn counts_more_than_the_threshold_apart_are_drift() {
1659 // Cloudflare counted 30,000 operations where g1t counted 10,000:
1660 // binding reads, perhaps. -66.7%.
1661 let drift = drifts("git", &[day("git", 3_000_000, 1_500_000, 1_800_000, 30_000.0, 10_000.0)], 10.0, true, 100_000);
1662 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Count, DriftKind::Cost]);
1663 assert!((drift[0].delta_percent.unwrap() + 66.666).abs() < 0.01);
1664 // 9% apart: within 10%.
1665 assert!(drifts("git", &[day("git", 1_000_000, 1_000_000, 1_200_000, 10_000.0, 10_900.0)], 10.0, true, 100_000).is_empty());
1666 // Uncounted products have no count drift.
1667 assert!(drifts("sandboxes", &[day("sandboxes", 1_000_000, 1_050_000, 1_200_000, 5.0, 0.0)], 10.0, false, 100_000).is_empty());
1668 }
1669
1670 #[test]
1671 fn cost_with_no_revenue_is_a_leak_but_not_for_running_g1t() {
1672 let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000);
1673 assert_eq!(leak.len(), 1);
1674 assert_eq!(leak[0].kind, DriftKind::Leak);
1675 assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1676 // Pennies say nothing.
1677 assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
1678 assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000)[0].kind == DriftKind::Leak);
1679 }
1680
1681 #[test]
1682 fn a_margin_alert_needs_n_days_in_a_row_under_the_floor() {
1683 let s = |d: &str, revenue: i64, cost: i64| (d.to_string(), revenue, cost);
1684 // 5%, 0%, -20%: three days under 10%.
1685 let series = vec![s("10-13", 1_200_000, 1_000_000), s("10-14", 1_050_000, 1_000_000), s("10-15", 1_000_000, 1_000_000), s("10-16", 1_000_000, 1_200_000)];
1686 let (from, worst) = breach(&series, 10.0, 3, 100_000).unwrap();
1687 assert_eq!(from, "10-14");
1688 assert!((worst + 20.0).abs() < 1e-9);
1689 // A good day in the window clears it.
1690 let mended = vec![s("10-14", 1_050_000, 1_000_000), s("10-15", 1_300_000, 1_000_000), s("10-16", 1_000_000, 1_200_000)];
1691 assert!(breach(&mended, 10.0, 3, 100_000).is_none());
1692 // Cost with no revenue at all is the worst margin there is.
1693 assert_eq!(breach(&[s("10-16", 0, 500_000)], 10.0, 1, 100_000).unwrap().1, -100.0);
1694 // Too little cost to judge.
1695 assert!(breach(&[s("10-16", 0, 5_000)], 10.0, 1, 100_000).is_none());
1696 assert!(breach(&series, 10.0, 9, 100_000).is_none());
1697 }
1698
1699 #[test]
1700 fn shared_costs_add_up_to_the_bill() {
1701 let w = |k: &str, v: f64| (k.to_string(), v);
1702 assert_eq!(attribute(100, &[w("a", 1.0), w("b", 1.0), w("c", 1.0)]), vec![("a".into(), 34), ("b".into(), 33), ("c".into(), 33)]);
1703 assert_eq!(attribute(10, &[w("a", 3.0), w("b", 1.0), w("a", 0.0)]), vec![("a".into(), 8), ("b".into(), 2)]);
1704 assert!(attribute(10, &[w("a", 0.0)]).is_empty());
1705 assert!(attribute(0, &[w("a", 1.0)]).is_empty());
1706 }
1707
1708 #[test]
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it1709 fn what_g1t_gives_away_is_kept_apart_from_what_it_sells() {
1710 let map = BTreeMap::new();
1711 // A comped workspace (all of it given), one in its trial (half paid
1712 // by the trial) and one paying in cash, all on models.
1713 let mut comped = usage("2026-10-15", "flagon", "agent", 1_200_000, 0, 1_000_000);
1714 comped.given = comped.value;
1715 let mut trial = usage("2026-10-15", "acme", "agent", 1_200_000, 600_000, 1_000_000);
1716 trial.given = 600_000;
1717 let paying = usage("2026-10-15", "beta", "agent", 1_200_000, 1_200_000, 1_000_000);
1718 let (days, workspaces) = fold(&[], &map, &[], &[], &[comped, trial, paying]);
1719 let models = days.iter().find(|d| d.bucket == "models").unwrap();
1720 assert_eq!((models.cost(), models.given_micros), (3_000_000, 1_500_000));
1721 let given = |w: &str| workspaces.iter().find(|x| x.workspace == w).unwrap().given;
1722 assert_eq!((given("flagon"), given("acme"), given("beta")), (1_000_000, 500_000, 0));
1723 }
1724
1725 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1726 fn a_workspace_that_costs_more_than_it_pays_is_flagged() {
1727 let rows = vec![("acme".to_string(), 5_000_000, 1_000_000), ("beta".to_string(), 900_000, 0), ("gamma".to_string(), 2_000_000, 3_000_000)];
1728 let found = anomalies(&rows, 1.0, 1_000_000);
1729 assert_eq!(found, vec![("acme".to_string(), 5_000_000, 1_000_000)]);
1730 // At twice its revenue as the threshold, $5 against $3 is fine.
1731 assert!(anomalies(&[("acme".to_string(), 5_000_000, 3_000_000)], 2.0, 1_000_000).is_empty());
1732 }
1733
1734 #[test]
1735 fn a_git_operation_costs_what_cloudflare_counts_for_it() {
1736 // $0.15 per 1,000 of Cloudflare's operations, on the charged days.
1737 let rate = billed_rate(&[(10_000.0, 0.0), (20_000.0, 3.0), (30_000.0, 4.5), (5_000.0, 0.75)]).unwrap();
1738 assert!((rate - 0.000_15).abs() < 1e-12);
1739 // Cloudflare counted 3 for every 1 g1t did: binding reads count.
1740 let per_op = derived_unit_cost(rate, 300_000.0, 100_000.0).unwrap();
1741 let per_thousand_micros = per_op * unit_size("1,000 operations") * 1e6;
1742 assert!((per_thousand_micros - 450_000.0).abs() < 1e-6, "{per_thousand_micros}");
1743 // Too few of g1t's units to say.
1744 assert!(derived_unit_cost(rate, 3_000.0, 500.0).is_none());
1745 assert!(billed_rate(&[(10_000.0, 0.0)]).is_none());
1746 assert_eq!(unit_size("million requests"), 1e6);
1747 assert_eq!(unit_size("second"), 1.0);
1748 }
1749}