* Sums one dimension over the range. * * On the `machines` metric: `daily_dim_counts.machines` is per day, so summing * it over a range gives **machine-days**, not distinct machines — a machine * seen on ten days counts ten times. A range-wide distinct count per dimension * value is not recover
(env: Env, url: URL, range: Range)
| 486 | * `max` is over a single row and the question does not arise. |
| 487 | */ |
| 488 | async function breakdown(env: Env, url: URL, range: Range): Promise<ApiResult> { |
| 489 | const dim = url.searchParams.get('dim') ?? ''; |
| 490 | const spec = DIMS[dim]; |
| 491 | if (!spec) return fail(`unknown dim — one of: ${Object.keys(DIMS).join(', ')}`); |
| 492 | |
| 493 | const requestedMetric = url.searchParams.get('metric'); |
| 494 | if (requestedMetric !== null && requestedMetric !== 'count' && requestedMetric !== 'machines') { |
| 495 | return fail('metric must be count or machines'); |
| 496 | } |
| 497 | const metric = requestedMetric ?? spec.metric; |
| 498 | |
| 499 | const rawLimit = url.searchParams.get('limit'); |
| 500 | const limit = rawLimit === null ? DEFAULT_BREAKDOWN_LIMIT : Number(rawLimit); |
| 501 | if (!Number.isInteger(limit) || limit < 1 || limit > MAX_BREAKDOWN_LIMIT) { |
| 502 | return fail(`limit must be an integer between 1 and ${MAX_BREAKDOWN_LIMIT}`); |
| 503 | } |
| 504 | |
| 505 | const event = url.searchParams.get('event') ?? spec.event ?? null; |
| 506 | if (event !== null && !/^[a-z_]{1,32}$/.test(event)) return fail('event must be a bare event name'); |
| 507 | |
| 508 | const binds: (string | number)[] = [dim, range.from, range.to]; |
| 509 | if (event !== null) binds.push(event); |
| 510 | |
| 511 | const { results } = await env.DB.prepare( |
| 512 | `SELECT value, sum(day_count) AS count, sum(day_machines) AS machines |
| 513 | FROM (SELECT day, value, sum(count) AS day_count, max(machines) AS day_machines |
| 514 | FROM daily_dim_counts |
| 515 | WHERE dim = ? AND day BETWEEN ? AND ?${event !== null ? ' AND event = ?' : ''} |
| 516 | GROUP BY day, value) |
| 517 | GROUP BY value`, |
| 518 | ) |
| 519 | .bind(...binds) |
| 520 | .all<BreakdownRow>(); |
| 521 | |
| 522 | const rows = results.map((r) => ({ |
| 523 | value: r.value, |
| 524 | count: r.count ?? 0, |
| 525 | machines: r.machines ?? 0, |
| 526 | })); |
| 527 | |
| 528 | const pick = (r: BreakdownRow): number => (metric === 'machines' ? r.machines : r.count); |
| 529 | |
| 530 | let ordered: BreakdownRow[]; |
| 531 | let truncated = false; |
| 532 | if (spec.order === 'bucket' && spec.buckets) { |
| 533 | // An ordered scale: the buckets keep their own order and all of them show, |
| 534 | // including empty ones, so the ordinal colour ramp always means the same thing. |
| 535 | const found = new Map(rows.map((r) => [r.value, r])); |
| 536 | const extra = rows.filter((r) => !spec.buckets?.includes(r.value)).sort((x, y) => pick(y) - pick(x)); |
| 537 | ordered = [ |
| 538 | ...spec.buckets.map((b) => found.get(b) ?? { value: b, count: 0, machines: 0 }), |
| 539 | ...extra, |
| 540 | ]; |
| 541 | } else { |
| 542 | const sorted = [...rows].sort( |
| 543 | spec.order === 'version_desc' |
| 544 | ? (x, y) => compareVersionsDesc(x.value, y.value) |
| 545 | : (x, y) => pick(y) - pick(x) || x.value.localeCompare(y.value), |