| 1 | import * as Alchemy from "alchemy"; |
| 2 | import * as Axiom from "alchemy/Axiom"; |
| 3 | import type { Input } from "alchemy/Input"; |
| 4 | import * as Output from "alchemy/Output"; |
| 5 | import { Effect } from "effect"; |
| 6 | import { Traces } from "./Datasets.ts"; |
| 7 | |
| 8 | /** |
| 9 | * `${stage} alchemy CLI overview` — a single dashboard answering the |
| 10 | * four questions we actually care about, laid out on a 12-column grid: |
| 11 | * |
| 12 | * 1. **How many active users are working on a project?** |
| 13 | * Distinct `alchemy.user.id` per `alchemy.git.origin_hash`. |
| 14 | * 2. **How many distinct projects are there?** |
| 15 | * Distinct `alchemy.git.origin_hash`. |
| 16 | * 3. **How many projects use CI/CD?** |
| 17 | * Distinct `alchemy.git.origin_hash` where `alchemy.ci=true`. |
| 18 | * 4. **What state stores are people using?** |
| 19 | * Sourced from `state_store.init` spans tagged with |
| 20 | * `alchemy.state_store.id` (the open-ended `StateService.id` |
| 21 | * slug; built-ins are `local` / `inmemory` / `http` / |
| 22 | * `cloudflare-http`, third-party stores get tracked automatically |
| 23 | * by setting their own slug). Emitted once per process via |
| 24 | * `recordStateStoreInit` at every `Layer.effect(State, …)` site. |
| 25 | * |
| 26 | * Project identity uses `alchemy.git.origin_hash` rather than |
| 27 | * `alchemy.user.id`: ephemeral CI runners regenerate `~/.alchemy/id` |
| 28 | * every job, which dramatically inflates the user count. The git |
| 29 | * origin hash is stable across runs of the same repo and is the |
| 30 | * closest proxy we have for "a project". |
| 31 | * |
| 32 | * All queries target `${stage}-traces` — Axiom's metrics datasets |
| 33 | * cannot be queried via APL, but every metric we emit has an |
| 34 | * equivalent span (`cli.<command>`, `provider.<op>`, |
| 35 | * `state_store.deploy`, `state_store.init`). |
| 36 | * |
| 37 | * Each chart's APL query is built with `Output.interpolate` against |
| 38 | * `traces.name` so Alchemy sequences the dashboard after the dataset |
| 39 | * exists. Otherwise Axiom would reject creation with |
| 40 | * `BadRequest: failed to validate ... entity not found`. |
| 41 | */ |
| 42 | export const CliOverviewDashboard = Axiom.Dashboard( |
| 43 | "CliOverview", |
| 44 | Effect.gen(function* () { |
| 45 | const stack = yield* Alchemy.Stack; |
| 46 | const traces = yield* Traces; |
| 47 | const t = traces.name; |
| 48 | const isProd = stack.stage === "prod"; |
| 49 | |
| 50 | const prodTracesName: Input<string> = isProd |
| 51 | ? t |
| 52 | : (yield* Axiom.Dataset.ref("Traces", { stage: "prod" })).name; |
| 53 | // Global filter bar — a `SmartFilter` chart drives every other |
| 54 | // chart's APL via `declare query_parameters` parameters: |
| 55 | // |
| 56 | // - `alchemy_version` (every stage) narrows every chart to a |
| 57 | // single alchemy version. An explicit "All" row with an empty |
| 58 | // value short-circuits the filter via |
| 59 | // `isempty(alchemy_version)`. APL-sourced filters don't get |
| 60 | // the auto-"All" that `list` filters do, so the source query |
| 61 | // unions one in. |
| 62 | // |
| 63 | // - `dataset_filter` (non-prod stages only) lets us preview |
| 64 | // dashboard changes against `prod-traces` while defaulting to |
| 65 | // the current stage's own dataset. Charts reference the |
| 66 | // dataset via `table(dataset_filter)` instead of a literal |
| 67 | // `['<name>']` so the parameter binding takes effect. On prod |
| 68 | // we skip the filter entirely and hardcode `['prod-traces']`. |
| 69 | // |
| 70 | // Fresh stages have no spans yet, so `service.version` may not |
| 71 | // exist as a column. `column_ifexists` lets the queries parse |
| 72 | // against an empty dataset (otherwise APL rejects them with |
| 73 | // `invalid field: "service.version"`). |
| 74 | // `bin_size` drives every TimeSeries chart's bucket width so a |
| 75 | // single filter flips the whole dashboard between hourly / daily / |
| 76 | // weekly aggregation. Queries reference it as |
| 77 | // `bin(_time, totimespan(bin_size))`. |
| 78 | const declarations: Input<string> = isProd |
| 79 | ? `declare query_parameters (alchemy_version:string = "", bin_size:string = "1h");` |
| 80 | : Output.interpolate`declare query_parameters (dataset_filter:string = "${t}", alchemy_version:string = "", bin_size:string = "1h");`; |
| 81 | const datasetExpr: Input<string> = isProd |
| 82 | ? Output.interpolate`['${t}']` |
| 83 | : "table(dataset_filter)"; |
| 84 | const versionFilterWhere = `\n | where isempty(alchemy_version) or tostring(column_ifexists("service.version", "")) == alchemy_version`; |
| 85 | |
| 86 | const charts: Input<Axiom.DashboardProps["dashboard"]["charts"]> = [ |
| 87 | { |
| 88 | id: "filter-bar", |
| 89 | type: "SmartFilter", |
| 90 | name: "Filters", |
| 91 | filters: [ |
| 92 | ...(isProd |
| 93 | ? [] |
| 94 | : [ |
| 95 | { |
| 96 | id: "dataset_filter", |
| 97 | type: "select" as const, |
| 98 | name: "dataset", |
| 99 | active: true, |
| 100 | selectType: "list" as const, |
| 101 | options: [ |
| 102 | { |
| 103 | key: `${stack.stage} (this stage)`, |
| 104 | value: t, |
| 105 | default: true, |
| 106 | }, |
| 107 | { |
| 108 | key: "prod (production data)", |
| 109 | value: prodTracesName, |
| 110 | }, |
| 111 | ], |
| 112 | }, |
| 113 | ]), |
| 114 | { |
| 115 | id: "bin_size", |
| 116 | type: "select", |
| 117 | name: "aggregation period", |
| 118 | active: true, |
| 119 | selectType: "list", |
| 120 | options: [ |
| 121 | { key: "1 hour", value: "1h", default: true }, |
| 122 | { key: "6 hours", value: "6h" }, |
| 123 | { key: "1 day", value: "1d" }, |
| 124 | { key: "7 days", value: "7d" }, |
| 125 | ], |
| 126 | }, |
| 127 | { |
| 128 | id: "alchemy_version", |
| 129 | type: "select", |
| 130 | name: "alchemy version", |
| 131 | active: true, |
| 132 | selectType: "query", |
| 133 | options: [], |
| 134 | query: { |
| 135 | apl: Output.interpolate` |
| 136 | ${declarations} |
| 137 | let opts = ${datasetExpr} |
| 138 | | extend sv = tostring(column_ifexists("service.version", "")) |
| 139 | | where sv != "" |
| 140 | | distinct sv |
| 141 | | sort by sv desc |
| 142 | | project key=sv, value=sv; |
| 143 | let all = print key="All", value=""; |
| 144 | union all, opts |
| 145 | `, |
| 146 | }, |
| 147 | }, |
| 148 | ], |
| 149 | }, |
| 150 | // Row 1 — top-line counts answering Qs 1, 2, 3. |
| 151 | { |
| 152 | id: "distinct-projects", |
| 153 | name: "Distinct projects (7d)", |
| 154 | type: "Statistic", |
| 155 | query: { |
| 156 | apl: Output.interpolate` |
| 157 | ${declarations} |
| 158 | ${datasetExpr}${versionFilterWhere} |
| 159 | | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']) |
| 160 | | where project != "" |
| 161 | | summarize projects=dcount(project) |
| 162 | `, |
| 163 | }, |
| 164 | }, |
| 165 | { |
| 166 | id: "projects-using-ci", |
| 167 | name: "Projects using CI/CD (7d)", |
| 168 | type: "Statistic", |
| 169 | query: { |
| 170 | apl: Output.interpolate` |
| 171 | ${declarations} |
| 172 | ${datasetExpr}${versionFilterWhere} |
| 173 | | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), |
| 174 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 175 | | where project != "" and ci == "true" |
| 176 | | summarize projects=dcount(project) |
| 177 | `, |
| 178 | }, |
| 179 | }, |
| 180 | { |
| 181 | id: "active-users-7d", |
| 182 | name: "Active users — non-CI (7d)", |
| 183 | type: "Statistic", |
| 184 | query: { |
| 185 | apl: Output.interpolate` |
| 186 | ${declarations} |
| 187 | ${datasetExpr}${versionFilterWhere} |
| 188 | | extend uid=tostring(['resource.custom']['alchemy.user.id']), |
| 189 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 190 | | where ci != "true" |
| 191 | | summarize users=dcount(uid) |
| 192 | `, |
| 193 | }, |
| 194 | }, |
| 195 | |
| 196 | // Row 2 — Q1 broken out per-project, plus solo-vs-team split. |
| 197 | { |
| 198 | id: "users-per-project", |
| 199 | name: "Active users per project (7d)", |
| 200 | type: "Table", |
| 201 | query: { |
| 202 | apl: Output.interpolate` |
| 203 | ${declarations} |
| 204 | ${datasetExpr}${versionFilterWhere} |
| 205 | | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), |
| 206 | uid=tostring(['resource.custom']['alchemy.user.id']), |
| 207 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 208 | | where project != "" |
| 209 | | summarize users_total=dcount(uid), |
| 210 | users_local=dcountif(uid, ci != "true"), |
| 211 | users_ci=dcountif(uid, ci == "true"), |
| 212 | events=count() |
| 213 | by project |
| 214 | | order by users_total desc |
| 215 | | take 100 |
| 216 | `, |
| 217 | }, |
| 218 | }, |
| 219 | { |
| 220 | id: "project-team-size-distribution", |
| 221 | name: "Project team-size distribution (local users / project)", |
| 222 | type: "Table", |
| 223 | query: { |
| 224 | apl: Output.interpolate` |
| 225 | ${declarations} |
| 226 | ${datasetExpr}${versionFilterWhere} |
| 227 | | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), |
| 228 | uid=tostring(['resource.custom']['alchemy.user.id']), |
| 229 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 230 | | where project != "" and ci != "true" |
| 231 | | summarize users=dcount(uid) by project |
| 232 | | extend bucket = case( |
| 233 | users == 1, "1 (solo)", |
| 234 | users <= 3, "2-3", |
| 235 | users <= 10, "4-10", |
| 236 | "11+") |
| 237 | | summarize projects=count() by bucket |
| 238 | | order by bucket asc |
| 239 | `, |
| 240 | }, |
| 241 | }, |
| 242 | |
| 243 | // Row 3 — Q4: state-store breakdown. |
| 244 | { |
| 245 | id: "state-store-projects-by-id", |
| 246 | name: "Projects by state store (7d)", |
| 247 | type: "Table", |
| 248 | query: { |
| 249 | apl: Output.interpolate` |
| 250 | ${declarations} |
| 251 | ${datasetExpr}${versionFilterWhere} |
| 252 | | where name == "state_store.init" |
| 253 | | extend store=tostring(['attributes.custom']['alchemy.state_store.id']), |
| 254 | project=tostring(['resource.custom']['alchemy.git.origin_hash']), |
| 255 | uid=tostring(['resource.custom']['alchemy.user.id']) |
| 256 | | summarize projects=dcountif(project, project != ""), |
| 257 | users=dcount(uid), |
| 258 | inits=count() |
| 259 | by store |
| 260 | | order by projects desc |
| 261 | `, |
| 262 | }, |
| 263 | }, |
| 264 | { |
| 265 | id: "state-store-by-id-over-time", |
| 266 | name: "State store init by store / hour", |
| 267 | type: "TimeSeries", |
| 268 | query: { |
| 269 | apl: Output.interpolate` |
| 270 | ${declarations} |
| 271 | ${datasetExpr}${versionFilterWhere} |
| 272 | | where name == "state_store.init" |
| 273 | | extend store=tostring(['attributes.custom']['alchemy.state_store.id']) |
| 274 | | summarize count=count() by store, bin(_time, totimespan(bin_size)) |
| 275 | | order by _time asc |
| 276 | `, |
| 277 | }, |
| 278 | }, |
| 279 | |
| 280 | // Row 4 — adoption shape: project growth + CI-vs-local split. |
| 281 | { |
| 282 | id: "projects-over-time", |
| 283 | name: "Distinct projects per day", |
| 284 | type: "TimeSeries", |
| 285 | query: { |
| 286 | apl: Output.interpolate` |
| 287 | ${declarations} |
| 288 | ${datasetExpr}${versionFilterWhere} |
| 289 | | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']) |
| 290 | | where project != "" |
| 291 | | summarize projects=dcount(project) by bin(_time, totimespan(bin_size)) |
| 292 | | order by _time asc |
| 293 | `, |
| 294 | }, |
| 295 | }, |
| 296 | { |
| 297 | id: "active-users-over-time-hourly", |
| 298 | name: "Active users (non-CI)", |
| 299 | type: "TimeSeries", |
| 300 | query: { |
| 301 | apl: Output.interpolate` |
| 302 | ${declarations} |
| 303 | ${datasetExpr}${versionFilterWhere} |
| 304 | | extend uid=tostring(['resource.custom']['alchemy.user.id']), |
| 305 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 306 | | where ci != "true" and uid != "" |
| 307 | | summarize users=dcount(uid) by bin(_time, totimespan(bin_size)) |
| 308 | | order by _time asc |
| 309 | `, |
| 310 | }, |
| 311 | }, |
| 312 | { |
| 313 | id: "active-users-over-time-daily", |
| 314 | name: "Active users (CI vs local)", |
| 315 | type: "TimeSeries", |
| 316 | query: { |
| 317 | apl: Output.interpolate` |
| 318 | ${declarations} |
| 319 | ${datasetExpr}${versionFilterWhere} |
| 320 | | extend uid=tostring(['resource.custom']['alchemy.user.id']), |
| 321 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 322 | | where uid != "" |
| 323 | | extend bucket=iff(ci == "true", "ci", "local") |
| 324 | | summarize users=dcount(uid) by bucket, bin(_time, totimespan(bin_size)) |
| 325 | | order by _time asc |
| 326 | `, |
| 327 | }, |
| 328 | }, |
| 329 | { |
| 330 | id: "ci-vs-local-projects", |
| 331 | name: "Projects: CI vs local per day", |
| 332 | type: "TimeSeries", |
| 333 | query: { |
| 334 | apl: Output.interpolate` |
| 335 | ${declarations} |
| 336 | ${datasetExpr}${versionFilterWhere} |
| 337 | | extend project=tostring(['resource.custom']['alchemy.git.origin_hash']), |
| 338 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 339 | | where project != "" |
| 340 | | extend bucket=iff(ci == "true", "ci", "local") |
| 341 | | summarize projects=dcount(project) by bucket, bin(_time, totimespan(bin_size)) |
| 342 | | order by _time asc |
| 343 | `, |
| 344 | }, |
| 345 | }, |
| 346 | |
| 347 | // Row 5 — keep the state-store deploy health signals for the |
| 348 | // Cloudflare-hosted store (not just init, but actual deploys). |
| 349 | { |
| 350 | id: "state-store-deploys", |
| 351 | name: "Cloudflare state store deploys (success vs error)", |
| 352 | type: "TimeSeries", |
| 353 | query: { |
| 354 | apl: Output.interpolate` |
| 355 | ${declarations} |
| 356 | ${datasetExpr}${versionFilterWhere} |
| 357 | | where name == "state_store.deploy" |
| 358 | | extend status=iff(tobool(['error']), "error", "success") |
| 359 | | summarize count=count() by status, bin(_time, totimespan(bin_size)) |
| 360 | | order by _time asc |
| 361 | `, |
| 362 | }, |
| 363 | }, |
| 364 | { |
| 365 | id: "active-users-by-version", |
| 366 | name: "Active users by alchemy version (7d)", |
| 367 | type: "Table", |
| 368 | query: { |
| 369 | apl: Output.interpolate` |
| 370 | ${declarations} |
| 371 | ${datasetExpr}${versionFilterWhere} |
| 372 | | extend uid=tostring(['resource.custom']['alchemy.user.id']), |
| 373 | version=tostring(['resource.custom']['alchemy.version']), |
| 374 | ci=tostring(['resource.custom']['alchemy.ci']) |
| 375 | | summarize users_total=dcount(uid), |
| 376 | users_local=dcountif(uid, ci != "true"), |
| 377 | users_ci=dcountif(uid, ci == "true") |
| 378 | by version |
| 379 | | order by users_total desc |
| 380 | `, |
| 381 | }, |
| 382 | }, |
| 383 | |
| 384 | // ─── Cloudflare State Store ──────────────────────────────── |
| 385 | // |
| 386 | // Two span sources feed this section: |
| 387 | // |
| 388 | // - **CLI spans** (`state_store.init`, `state_store.bootstrap`, |
| 389 | // `state_store.deploy`, ...) — emitted from the user's |
| 390 | // terminal/CI. Carry `alchemy.cloudflare.account_hash` (a |
| 391 | // SHA-256 of the CF accountId) so we can count distinct |
| 392 | // deployments without leaking raw account IDs. |
| 393 | // |
| 394 | // - **Worker handler spans** (`state_store.{getVersion, |
| 395 | // listStacks,listStages,listResources,getState,setState, |
| 396 | // deleteState,getReplacedResources,deleteStack}`) — emitted |
| 397 | // from inside the deployed `alchemy-state-store` worker. |
| 398 | // Distinguished by the `alchemy.state_store.script_name` |
| 399 | // **resource** attribute (set on the worker's OTLP tracer |
| 400 | // resource); CLI spans do not carry that resource attr. |
| 401 | // |
| 402 | // Worker handler spans carry `alchemy.state_store.{op,stack, |
| 403 | // stage,fqn}` as span attributes plus a `duration` field — |
| 404 | // that's what powers the latency / op-rate / stack-count |
| 405 | // charts. Account-hash sits on CLI spans only (worker-side |
| 406 | // injection would need a separate deploy-time env-var pass). |
| 407 | { |
| 408 | id: "state-store-distinct-cloudflare-stores", |
| 409 | name: "Distinct Cloudflare state-store deployments (7d)", |
| 410 | type: "Statistic", |
| 411 | query: { |
| 412 | apl: Output.interpolate` |
| 413 | ${declarations} |
| 414 | ${datasetExpr}${versionFilterWhere} |
| 415 | | where name == "state_store.init" |
| 416 | | extend hash=tostring(['attributes.custom']['alchemy.cloudflare.account_hash']) |
| 417 | | where hash != "" |
| 418 | | summarize stores=dcount(hash) |
| 419 | `, |
| 420 | }, |
| 421 | }, |
| 422 | { |
| 423 | id: "state-store-distinct-stacks", |
| 424 | name: "Distinct stacks tracked (7d)", |
| 425 | type: "Statistic", |
| 426 | query: { |
| 427 | apl: Output.interpolate` |
| 428 | ${declarations} |
| 429 | ${datasetExpr}${versionFilterWhere} |
| 430 | | where ['resource.custom']['alchemy.state_store.script_name'] != "" |
| 431 | | extend stack=tostring(['attributes.custom']['alchemy.state_store.stack']) |
| 432 | | where stack != "" |
| 433 | | summarize stacks=dcount(stack) |
| 434 | `, |
| 435 | }, |
| 436 | }, |
| 437 | { |
| 438 | id: "state-store-total-ops", |
| 439 | name: "Cloudflare state store ops (7d)", |
| 440 | type: "Statistic", |
| 441 | query: { |
| 442 | apl: Output.interpolate` |
| 443 | ${declarations} |
| 444 | ${datasetExpr}${versionFilterWhere} |
| 445 | | where ['resource.custom']['alchemy.state_store.script_name'] != "" |
| 446 | | extend op=tostring(['attributes.custom']['alchemy.state_store.op']) |
| 447 | | where op != "" |
| 448 | | summarize ops=count() |
| 449 | `, |
| 450 | }, |
| 451 | }, |
| 452 | { |
| 453 | id: "state-store-op-latency", |
| 454 | name: "State store op latency (p50 / p95 / p99, 7d)", |
| 455 | type: "Table", |
| 456 | query: { |
| 457 | apl: Output.interpolate` |
| 458 | ${declarations} |
| 459 | ${datasetExpr}${versionFilterWhere} |
| 460 | | where ['resource.custom']['alchemy.state_store.script_name'] != "" |
| 461 | | extend op=tostring(['attributes.custom']['alchemy.state_store.op']) |
| 462 | | where op != "" |
| 463 | | summarize p50=percentile(duration, 50), |
| 464 | p95=percentile(duration, 95), |
| 465 | p99=percentile(duration, 99), |
| 466 | calls=count() |
| 467 | by op |
| 468 | | order by calls desc |
| 469 | `, |
| 470 | }, |
| 471 | }, |
| 472 | { |
| 473 | id: "state-store-ops-over-time", |
| 474 | name: "State store ops over time (by op)", |
| 475 | type: "TimeSeries", |
| 476 | query: { |
| 477 | apl: Output.interpolate` |
| 478 | ${declarations} |
| 479 | ${datasetExpr}${versionFilterWhere} |
| 480 | | where ['resource.custom']['alchemy.state_store.script_name'] != "" |
| 481 | | extend op=tostring(['attributes.custom']['alchemy.state_store.op']) |
| 482 | | where op != "" |
| 483 | | summarize count=count() by op, bin(_time, totimespan(bin_size)) |
| 484 | | order by _time asc |
| 485 | `, |
| 486 | }, |
| 487 | }, |
| 488 | { |
| 489 | id: "state-store-error-rate", |
| 490 | name: "State store error rate by op (7d)", |
| 491 | type: "Table", |
| 492 | query: { |
| 493 | apl: Output.interpolate` |
| 494 | ${declarations} |
| 495 | ${datasetExpr}${versionFilterWhere} |
| 496 | | where ['resource.custom']['alchemy.state_store.script_name'] != "" |
| 497 | | extend op=tostring(['attributes.custom']['alchemy.state_store.op']), |
| 498 | err=tobool(['error']) |
| 499 | | where op != "" |
| 500 | | summarize errors=countif(err == true), |
| 501 | total=count() |
| 502 | by op |
| 503 | | extend error_rate=todouble(errors) / todouble(total) |
| 504 | | order by total desc |
| 505 | `, |
| 506 | }, |
| 507 | }, |
| 508 | { |
| 509 | id: "state-store-top-stacks", |
| 510 | name: "Top stacks by activity (7d)", |
| 511 | type: "Table", |
| 512 | query: { |
| 513 | apl: Output.interpolate` |
| 514 | ${declarations} |
| 515 | ${datasetExpr}${versionFilterWhere} |
| 516 | | where ['resource.custom']['alchemy.state_store.script_name'] != "" |
| 517 | | extend stack=tostring(['attributes.custom']['alchemy.state_store.stack']) |
| 518 | | where stack != "" |
| 519 | | summarize ops=count() by stack |
| 520 | | order by ops desc |
| 521 | | take 25 |
| 522 | `, |
| 523 | }, |
| 524 | }, |
| 525 | { |
| 526 | id: "state-store-deployments-over-time", |
| 527 | name: "Distinct Cloudflare state-store deployments per day", |
| 528 | type: "TimeSeries", |
| 529 | query: { |
| 530 | apl: Output.interpolate` |
| 531 | ${declarations} |
| 532 | ${datasetExpr}${versionFilterWhere} |
| 533 | | where name == "state_store.init" |
| 534 | | extend hash=tostring(['attributes.custom']['alchemy.cloudflare.account_hash']) |
| 535 | | where hash != "" |
| 536 | | summarize stores=dcount(hash) by bin(_time, totimespan(bin_size)) |
| 537 | | order by _time asc |
| 538 | `, |
| 539 | }, |
| 540 | }, |
| 541 | |
| 542 | // ─── Resource usage & reliability ────────────────────────── |
| 543 | // |
| 544 | // Every provider lifecycle invocation is wrapped in a |
| 545 | // `provider.<op>` span (see `instrumentLifecycle` in Apply.ts) |
| 546 | // carrying `alchemy.resource.type` and `alchemy.resource.op` |
| 547 | // (precreate/create/update/delete/read). `error` is set when |
| 548 | // the lifecycle effect fails. |
| 549 | { |
| 550 | id: "resource-usage-ranked", |
| 551 | name: "Resource usage — ranked by lifecycle ops (7d)", |
| 552 | type: "Table", |
| 553 | query: { |
| 554 | apl: Output.interpolate` |
| 555 | ${declarations} |
| 556 | ${datasetExpr}${versionFilterWhere} |
| 557 | | where name startswith "provider." |
| 558 | | extend resource_type=tostring(['attributes.custom']['alchemy.resource.type']), |
| 559 | project=tostring(['resource.custom']['alchemy.git.origin_hash']) |
| 560 | | where resource_type != "" |
| 561 | | summarize ops=count(), |
| 562 | projects=dcountif(project, project != ""), |
| 563 | errors=countif(tobool(['error']) == true) |
| 564 | by resource_type |
| 565 | | order by ops desc |
| 566 | | take 200 |
| 567 | `, |
| 568 | }, |
| 569 | }, |
| 570 | { |
| 571 | id: "resource-error-rate-by-type", |
| 572 | name: "Resource error rate by resource type (7d)", |
| 573 | type: "Table", |
| 574 | query: { |
| 575 | apl: Output.interpolate` |
| 576 | ${declarations} |
| 577 | ${datasetExpr}${versionFilterWhere} |
| 578 | | where name startswith "provider." |
| 579 | | extend resource_type=tostring(['attributes.custom']['alchemy.resource.type']), |
| 580 | err=tobool(['error']) |
| 581 | | where resource_type != "" |
| 582 | | summarize errors=countif(err == true), |
| 583 | total=count() |
| 584 | by resource_type |
| 585 | | extend error_rate=todouble(errors) / todouble(total) |
| 586 | | order by error_rate desc, total desc |
| 587 | | take 200 |
| 588 | `, |
| 589 | }, |
| 590 | }, |
| 591 | { |
| 592 | id: "resource-error-rate-by-op", |
| 593 | name: "Resource error rate by lifecycle method (7d)", |
| 594 | type: "Table", |
| 595 | query: { |
| 596 | apl: Output.interpolate` |
| 597 | ${declarations} |
| 598 | ${datasetExpr}${versionFilterWhere} |
| 599 | | where name startswith "provider." |
| 600 | | extend op=tostring(['attributes.custom']['alchemy.resource.op']), |
| 601 | err=tobool(['error']) |
| 602 | | where op != "" |
| 603 | | summarize errors=countif(err == true), |
| 604 | total=count() |
| 605 | by op |
| 606 | | extend error_rate=todouble(errors) / todouble(total) |
| 607 | | order by total desc |
| 608 | `, |
| 609 | }, |
| 610 | }, |
| 611 | ]; |
| 612 | |
| 613 | const layout: Axiom.DashboardProps["dashboard"]["layout"] = [ |
| 614 | // Row 0 — filter bar (full width, narrow) |
| 615 | { i: "filter-bar", x: 0, y: 0, w: 12, h: 2 }, |
| 616 | // Row 1 |
| 617 | { i: "distinct-projects", x: 0, y: 2, w: 4, h: 4 }, |
| 618 | { i: "projects-using-ci", x: 4, y: 2, w: 4, h: 4 }, |
| 619 | { i: "active-users-7d", x: 8, y: 2, w: 4, h: 4 }, |
| 620 | // Row 2 — active users over time |
| 621 | { i: "active-users-over-time-hourly", x: 0, y: 6, w: 6, h: 6 }, |
| 622 | { i: "active-users-over-time-daily", x: 6, y: 6, w: 6, h: 6 }, |
| 623 | // Row 3 |
| 624 | { i: "users-per-project", x: 0, y: 12, w: 8, h: 8 }, |
| 625 | { i: "project-team-size-distribution", x: 8, y: 12, w: 4, h: 8 }, |
| 626 | // Row 4 |
| 627 | { i: "state-store-projects-by-id", x: 0, y: 20, w: 6, h: 6 }, |
| 628 | { i: "state-store-by-id-over-time", x: 6, y: 20, w: 6, h: 6 }, |
| 629 | // Row 5 |
| 630 | { i: "projects-over-time", x: 0, y: 26, w: 6, h: 6 }, |
| 631 | { i: "ci-vs-local-projects", x: 6, y: 26, w: 6, h: 6 }, |
| 632 | // Row 6 |
| 633 | { i: "state-store-deploys", x: 0, y: 32, w: 6, h: 6 }, |
| 634 | { i: "active-users-by-version", x: 6, y: 32, w: 6, h: 6 }, |
| 635 | // Row 7 — Cloudflare State Store: top-line stats |
| 636 | { |
| 637 | i: "state-store-distinct-cloudflare-stores", |
| 638 | x: 0, |
| 639 | y: 38, |
| 640 | w: 4, |
| 641 | h: 4, |
| 642 | }, |
| 643 | { i: "state-store-distinct-stacks", x: 4, y: 38, w: 4, h: 4 }, |
| 644 | { i: "state-store-total-ops", x: 8, y: 38, w: 4, h: 4 }, |
| 645 | // Row 8 — performance: latency table (full width) |
| 646 | { i: "state-store-op-latency", x: 0, y: 42, w: 12, h: 8 }, |
| 647 | // Row 9 — ops over time + error rate |
| 648 | { i: "state-store-ops-over-time", x: 0, y: 50, w: 8, h: 6 }, |
| 649 | { i: "state-store-error-rate", x: 8, y: 50, w: 4, h: 6 }, |
| 650 | // Row 10 — distribution + adoption shape |
| 651 | { i: "state-store-top-stacks", x: 0, y: 56, w: 6, h: 8 }, |
| 652 | { i: "state-store-deployments-over-time", x: 6, y: 56, w: 6, h: 8 }, |
| 653 | // Row 11 — Resource usage ranking (full width) |
| 654 | { i: "resource-usage-ranked", x: 0, y: 64, w: 12, h: 10 }, |
| 655 | // Row 12 — Error rate broken down by resource type and lifecycle method |
| 656 | { i: "resource-error-rate-by-type", x: 0, y: 74, w: 8, h: 10 }, |
| 657 | { i: "resource-error-rate-by-op", x: 8, y: 74, w: 4, h: 10 }, |
| 658 | ]; |
| 659 | |
| 660 | return { |
| 661 | dashboard: { |
| 662 | name: `${stack.stage} alchemy CLI overview`, |
| 663 | // Empty owner = X-AXIOM-EVERYONE (org-shared). API tokens |
| 664 | // can't create per-user "private" dashboards. |
| 665 | owner: "", |
| 666 | description: [ |
| 667 | "Adoption telemetry: distinct projects, active users per project, ", |
| 668 | "CI vs local usage, and state-store backend breakdown. Plus ", |
| 669 | "Cloudflare State Store ops: distinct deployments, stack count, ", |
| 670 | "per-op latency / error rate. ", |
| 671 | "Resource usage: ranked list of resource types by lifecycle ops, ", |
| 672 | "with error rates broken down by resource type and by lifecycle method. ", |
| 673 | "Project identity = alchemy.git.origin_hash (stable across CI runs); ", |
| 674 | "user identity = alchemy.user.id (ephemeral in CI); ", |
| 675 | "Cloudflare deployment identity = alchemy.cloudflare.account_hash ", |
| 676 | "(SHA-256 of accountId, set on CLI state_store.* spans).", |
| 677 | ].join(""), |
| 678 | refreshTime: 60 as const, |
| 679 | schemaVersion: 2 as const, |
| 680 | // Axiom requires the `qr-now-{duration}` form for relative times. |
| 681 | timeWindowStart: "qr-now-7d", |
| 682 | timeWindowEnd: "qr-now", |
| 683 | charts, |
| 684 | layout, |
| 685 | }, |
| 686 | }; |
| 687 | }), |
| 688 | ); |