@burn0/burn0 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,14 @@ LLMs, SaaS, infrastructure. See per-request costs in real time.<br><br>
29
29
 
30
30
  ---
31
31
 
32
+
33
+ ### 🎬 See it in action
34
+
35
+
36
+ https://github.com/user-attachments/assets/56962fc8-b9cf-49b2-9481-bc10aca6fb56
37
+
38
+ </div>
39
+
32
40
  ## The Problem
33
41
 
34
42
  You're running OpenAI, Anthropic, Stripe, Supabase, SendGrid, and a dozen other APIs. Your monthly bill is $2,847 and climbing 340% month-over-month.
@@ -86,7 +94,7 @@ echo 'BURN0_API_KEY=b0_sk_your_key_here' >> .env
86
94
  # 3. Restart — costs now sync to burn0.dev
87
95
  ```
88
96
 
89
- Now you get a **live event feed**, **cost breakdown by service**, **monthly projections**, and **full request history** — all at [burn0.dev/dashboard](https://burn0.dev/dashboard).
97
+ Now you get a **live event feed**, **cost breakdown by service**, **monthly projections**, and **full request history** — all at [burn0.dev/dashboard](https://burn0## The Problem.dev/dashboard).
90
98
 
91
99
  > burn0 only syncs metadata (service, model, tokens, cost, latency) — never request/response bodies or your API keys.
92
100
 
@@ -397,32 +397,10 @@ function createRestorer(deps) {
397
397
  // src/transport/dispatcher.ts
398
398
  function createDispatcher(mode2, deps) {
399
399
  return (event) => {
400
- switch (mode2) {
401
- case "dev-local":
402
- deps.logEvent?.(event);
403
- deps.writeLedger?.(event);
404
- break;
405
- case "dev-cloud":
406
- deps.logEvent?.(event);
407
- deps.writeLedger?.(event);
408
- deps.addToBatch?.(event);
409
- break;
410
- case "prod-cloud":
411
- deps.logEvent?.(event);
412
- deps.writeLedger?.(event);
413
- deps.addToBatch?.(event);
414
- break;
415
- case "prod-local":
416
- deps.logEvent?.(event);
417
- break;
418
- case "test-enabled":
419
- deps.logEvent?.(event);
420
- deps.writeLedger?.(event);
421
- deps.addToBatch?.(event);
422
- break;
423
- case "test-disabled":
424
- break;
425
- }
400
+ if (mode2 === "test-disabled") return;
401
+ deps.logEvent?.(event);
402
+ deps.writeLedger?.(event);
403
+ deps.addToBatch?.(event);
426
404
  };
427
405
  }
428
406
 
@@ -468,6 +446,7 @@ import fs from "fs";
468
446
  import path from "path";
469
447
  var BURN0_DIR = ".burn0";
470
448
  var LEDGER_FILE = "costs.jsonl";
449
+ var SYNC_MARKER_FILE = "last-sync.txt";
471
450
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
472
451
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
473
452
  var LocalLedger = class {
@@ -491,6 +470,24 @@ var LocalLedger = class {
491
470
  return [];
492
471
  }
493
472
  }
473
+ readUnsynced() {
474
+ const all = this.read();
475
+ const lastSync = this.getLastSyncTime();
476
+ if (!lastSync) return all;
477
+ return all.filter((e) => new Date(e.timestamp).getTime() > lastSync);
478
+ }
479
+ markSynced() {
480
+ this.ensureDir();
481
+ fs.writeFileSync(path.join(this.dirPath, SYNC_MARKER_FILE), (/* @__PURE__ */ new Date()).toISOString());
482
+ }
483
+ getLastSyncTime() {
484
+ try {
485
+ const ts = fs.readFileSync(path.join(this.dirPath, SYNC_MARKER_FILE), "utf-8").trim();
486
+ return new Date(ts).getTime();
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
494
491
  ensureDir() {
495
492
  if (!fs.existsSync(this.dirPath)) fs.mkdirSync(this.dirPath, { recursive: true });
496
493
  }
@@ -535,10 +532,17 @@ async function shipEvents(events, apiKey2, baseUrl, fetchFn = globalThis.fetch)
535
532
  },
536
533
  body: JSON.stringify({ events, sdk_version: SDK_VERSION })
537
534
  });
538
- if (response.ok) return true;
535
+ if (response.ok) {
536
+ if (isDebug()) console.log(`[burn0] Shipped ${events.length} events`);
537
+ return true;
538
+ }
539
+ if (isDebug()) {
540
+ const body = await response.text().catch(() => "");
541
+ console.warn(`[burn0] Shipping rejected: ${response.status} ${body}`);
542
+ }
539
543
  } catch (err) {
540
544
  if (isDebug()) {
541
- console.warn("[burn0] Event shipping failed:", err.message);
545
+ console.warn("[burn0] Shipping failed:", err.message);
542
546
  }
543
547
  }
544
548
  if (attempt < maxAttempts - 1) {
@@ -796,31 +800,100 @@ try {
796
800
  }
797
801
  var ticker = createTicker({ todayCost, todayCalls, perServiceCosts });
798
802
  var batch = null;
799
- if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
800
- batch = new BatchBuffer({
803
+ var lateInitDone = false;
804
+ var failedEvents = [];
805
+ function createBatch(key) {
806
+ return new BatchBuffer({
801
807
  sizeThreshold: 50,
802
808
  timeThresholdMs: 1e4,
803
809
  maxSize: 500,
804
810
  onFlush: (events) => {
805
- shipEvents(events, apiKey, BURN0_API_URL, originalFetch2).catch(() => {
811
+ const toShip = failedEvents.length > 0 ? [...failedEvents, ...events] : events;
812
+ failedEvents = [];
813
+ shipEvents(toShip, key, BURN0_API_URL, originalFetch2).then((ok) => {
814
+ if (!ok) {
815
+ failedEvents = toShip.slice(-500);
816
+ }
817
+ }).catch(() => {
818
+ failedEvents = toShip.slice(-500);
806
819
  });
807
820
  }
808
821
  });
809
822
  }
823
+ if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
824
+ batch = createBatch(apiKey);
825
+ }
826
+ var pendingEvents = [];
827
+ function lateInit(event) {
828
+ if (batch) {
829
+ return;
830
+ }
831
+ const lateKey = getApiKey();
832
+ if (!lateKey) {
833
+ if (event) pendingEvents.push(event);
834
+ if (!lateInitDone) {
835
+ lateInitDone = true;
836
+ setTimeout(() => {
837
+ lateInitDone = false;
838
+ if (pendingEvents.length > 0) {
839
+ const e = pendingEvents.shift();
840
+ lateInit(e);
841
+ } else {
842
+ lateInit();
843
+ }
844
+ }, 0);
845
+ }
846
+ return;
847
+ }
848
+ lateInitDone = true;
849
+ apiKey = lateKey;
850
+ mode = detectMode({ isTTY: isTTY(), apiKey });
851
+ batch = createBatch(lateKey);
852
+ fetchPricing(BURN0_API_URL, originalFetch2).catch(() => {
853
+ });
854
+ for (const e of pendingEvents) {
855
+ batch.add(e);
856
+ }
857
+ pendingEvents.length = 0;
858
+ syncLedger(lateKey);
859
+ }
860
+ function syncLedger(key) {
861
+ try {
862
+ const unsynced = ledger.readUnsynced();
863
+ if (unsynced.length === 0) {
864
+ ledger.markSynced();
865
+ return;
866
+ }
867
+ const promises = [];
868
+ for (let i = 0; i < unsynced.length; i += 500) {
869
+ const chunk = unsynced.slice(i, i + 500);
870
+ promises.push(shipEvents(chunk, key, BURN0_API_URL, originalFetch2));
871
+ }
872
+ Promise.all(promises).then((results) => {
873
+ if (results.every(Boolean)) {
874
+ ledger.markSynced();
875
+ }
876
+ }).catch(() => {
877
+ });
878
+ } catch {
879
+ }
880
+ }
810
881
  var shouldWriteLedger = mode !== "test-disabled" && mode !== "prod-local";
811
882
  var dispatch = createDispatcher(mode, {
812
883
  logEvent: (e) => ticker.tick(e),
813
884
  writeLedger: shouldWriteLedger ? (e) => ledger.write(e) : void 0,
814
- addToBatch: batch ? (e) => batch.add(e) : void 0
885
+ addToBatch: (e) => {
886
+ lateInit();
887
+ batch?.add(e);
888
+ }
815
889
  });
816
890
  var preloaded = checkImportOrder();
817
891
  if (preloaded.length > 0) {
818
- console.warn(`[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`);
819
- }
820
- if (mode === "prod-local") {
821
- console.warn("[burn0] No API key \u2014 costs not tracked. Get one free at burn0.dev/api");
892
+ console.warn(
893
+ `[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`
894
+ );
822
895
  }
823
- if (canPatch() && mode !== "test-disabled" && mode !== "prod-local") {
896
+ if (canPatch() && mode !== "test-disabled") {
824
897
  const onEvent = (event) => {
825
898
  const enriched = enrichEvent(event);
826
899
  dispatch(enriched);
@@ -846,4 +919,4 @@ export {
846
919
  startSpan,
847
920
  restore
848
921
  };
849
- //# sourceMappingURL=chunk-H3A5NM5C.mjs.map
922
+ //# sourceMappingURL=chunk-UI6QVWA4.mjs.map
package/dist/cli/index.js CHANGED
@@ -18080,11 +18080,15 @@ async function _runInit() {
18080
18080
  }
18081
18081
  const serviceConfigs = [];
18082
18082
  if (allDetected.length > 0) {
18083
- console.log(source_default.bold(` Auto-detected ${allDetected.length} services:
18084
- `));
18083
+ console.log(
18084
+ source_default.bold(` Auto-detected ${allDetected.length} services:
18085
+ `)
18086
+ );
18085
18087
  for (const svc of allDetected) {
18086
18088
  const tag = svc.autopriced ? source_default.dim("auto-priced") : source_default.yellow("needs plan");
18087
- console.log(` ${source_default.green(" \u2713")} ${svc.displayName.padEnd(20)} ${tag}`);
18089
+ console.log(
18090
+ ` ${source_default.green(" \u2713")} ${svc.displayName.padEnd(20)} ${tag}`
18091
+ );
18088
18092
  }
18089
18093
  console.log();
18090
18094
  const fixedTier = allDetected.filter((s) => !s.autopriced);
@@ -18101,7 +18105,11 @@ async function _runInit() {
18101
18105
  });
18102
18106
  if (plan !== "skip") {
18103
18107
  const selected = entry.plans.find((p) => p.value === plan);
18104
- serviceConfigs.push({ name: svc.name, plan, monthlyCost: selected?.monthly });
18108
+ serviceConfigs.push({
18109
+ name: svc.name,
18110
+ plan,
18111
+ monthlyCost: selected?.monthly
18112
+ });
18105
18113
  } else {
18106
18114
  serviceConfigs.push({ name: svc.name });
18107
18115
  }
@@ -18120,18 +18128,38 @@ async function _runInit() {
18120
18128
  });
18121
18129
  if (addMore) {
18122
18130
  const alreadyAdded = new Set(serviceConfigs.map((s) => s.name));
18123
- const additionalServices = SERVICE_CATALOG.filter((s) => !alreadyAdded.has(s.name));
18131
+ const additionalServices = SERVICE_CATALOG.filter(
18132
+ (s) => !alreadyAdded.has(s.name)
18133
+ );
18124
18134
  const llmChoices = additionalServices.filter((s) => s.category === "llm").map((s) => ({ name: s.displayName, value: s.name }));
18125
18135
  const apiChoices = additionalServices.filter((s) => s.category === "api").map((s) => ({ name: s.displayName, value: s.name }));
18126
18136
  const infraChoices = additionalServices.filter((s) => s.category === "infra").map((s) => ({ name: s.displayName, value: s.name }));
18127
18137
  const additional = await esm_default2({
18128
18138
  message: "Select services:",
18129
18139
  choices: [
18130
- ...llmChoices.length ? [{ name: source_default.bold.blue("\u2500\u2500 LLM Providers \u2500\u2500"), value: "__sep", disabled: true }] : [],
18140
+ ...llmChoices.length ? [
18141
+ {
18142
+ name: source_default.bold.blue("\u2500\u2500 LLM Providers \u2500\u2500"),
18143
+ value: "__sep",
18144
+ disabled: true
18145
+ }
18146
+ ] : [],
18131
18147
  ...llmChoices,
18132
- ...apiChoices.length ? [{ name: source_default.bold.magenta("\u2500\u2500 API Services \u2500\u2500"), value: "__sep2", disabled: true }] : [],
18148
+ ...apiChoices.length ? [
18149
+ {
18150
+ name: source_default.bold.magenta("\u2500\u2500 API Services \u2500\u2500"),
18151
+ value: "__sep2",
18152
+ disabled: true
18153
+ }
18154
+ ] : [],
18133
18155
  ...apiChoices,
18134
- ...infraChoices.length ? [{ name: source_default.bold.yellow("\u2500\u2500 Infrastructure \u2500\u2500"), value: "__sep3", disabled: true }] : [],
18156
+ ...infraChoices.length ? [
18157
+ {
18158
+ name: source_default.bold.yellow("\u2500\u2500 Infrastructure \u2500\u2500"),
18159
+ value: "__sep3",
18160
+ disabled: true
18161
+ }
18162
+ ] : [],
18135
18163
  ...infraChoices
18136
18164
  ]
18137
18165
  });
@@ -18159,7 +18187,9 @@ async function _runInit() {
18159
18187
  }
18160
18188
  let projectName = "my-project";
18161
18189
  try {
18162
- const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path6.default.join(cwd, "package.json"), "utf-8"));
18190
+ const pkg = JSON.parse(
18191
+ import_node_fs5.default.readFileSync(import_node_path6.default.join(cwd, "package.json"), "utf-8")
18192
+ );
18163
18193
  if (pkg.name) projectName = pkg.name;
18164
18194
  } catch {
18165
18195
  }
@@ -18174,12 +18204,12 @@ async function _runInit() {
18174
18204
  });
18175
18205
  if (apiKey) {
18176
18206
  try {
18177
- const apiUrl = process.env.BURN0_API_URL ?? "https://burn0-server-production.up.railway.app";
18207
+ const apiUrl = "https://burn0-server-production.up.railway.app";
18178
18208
  const res = await fetch(`${apiUrl}/v1/projects/config`, {
18179
18209
  method: "POST",
18180
18210
  headers: {
18181
18211
  "Content-Type": "application/json",
18182
- "Authorization": `Bearer ${apiKey}`
18212
+ Authorization: `Bearer ${apiKey}`
18183
18213
  },
18184
18214
  body: JSON.stringify({
18185
18215
  services: serviceConfigs.map((s) => ({
@@ -18206,7 +18236,9 @@ async function _runInit() {
18206
18236
  const gitignoreLines = gitignoreContent.split("\n").map((l) => l.trim());
18207
18237
  if (!gitignoreLines.includes(".env")) {
18208
18238
  ensureGitignore(cwd, ".env");
18209
- console.log(source_default.green(" \u2713 Added .env to .gitignore (protects your API keys)"));
18239
+ console.log(
18240
+ source_default.green(" \u2713 Added .env to .gitignore (protects your API keys)")
18241
+ );
18210
18242
  }
18211
18243
  console.log("");
18212
18244
  console.log(source_default.green(" \u2713 Setup complete"));
@@ -18318,7 +18350,7 @@ var init_connect = __esm({
18318
18350
  });
18319
18351
 
18320
18352
  // src/transport/local.ts
18321
- var import_node_fs6, import_node_path7, BURN0_DIR, LEDGER_FILE, MAX_FILE_SIZE, MAX_AGE_MS, LocalLedger;
18353
+ var import_node_fs6, import_node_path7, BURN0_DIR, LEDGER_FILE, SYNC_MARKER_FILE, MAX_FILE_SIZE, MAX_AGE_MS, LocalLedger;
18322
18354
  var init_local = __esm({
18323
18355
  "src/transport/local.ts"() {
18324
18356
  "use strict";
@@ -18326,6 +18358,7 @@ var init_local = __esm({
18326
18358
  import_node_path7 = __toESM(require("path"));
18327
18359
  BURN0_DIR = ".burn0";
18328
18360
  LEDGER_FILE = "costs.jsonl";
18361
+ SYNC_MARKER_FILE = "last-sync.txt";
18329
18362
  MAX_FILE_SIZE = 10 * 1024 * 1024;
18330
18363
  MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
18331
18364
  LocalLedger = class {
@@ -18349,6 +18382,24 @@ var init_local = __esm({
18349
18382
  return [];
18350
18383
  }
18351
18384
  }
18385
+ readUnsynced() {
18386
+ const all = this.read();
18387
+ const lastSync = this.getLastSyncTime();
18388
+ if (!lastSync) return all;
18389
+ return all.filter((e) => new Date(e.timestamp).getTime() > lastSync);
18390
+ }
18391
+ markSynced() {
18392
+ this.ensureDir();
18393
+ import_node_fs6.default.writeFileSync(import_node_path7.default.join(this.dirPath, SYNC_MARKER_FILE), (/* @__PURE__ */ new Date()).toISOString());
18394
+ }
18395
+ getLastSyncTime() {
18396
+ try {
18397
+ const ts = import_node_fs6.default.readFileSync(import_node_path7.default.join(this.dirPath, SYNC_MARKER_FILE), "utf-8").trim();
18398
+ return new Date(ts).getTime();
18399
+ } catch {
18400
+ return null;
18401
+ }
18402
+ }
18352
18403
  ensureDir() {
18353
18404
  if (!import_node_fs6.default.existsSync(this.dirPath)) import_node_fs6.default.mkdirSync(this.dirPath, { recursive: true });
18354
18405
  }
@@ -18483,7 +18534,20 @@ function formatCost(cost) {
18483
18534
  }
18484
18535
  function formatDateLabel(dateStr) {
18485
18536
  const d = /* @__PURE__ */ new Date(dateStr + "T12:00:00");
18486
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
18537
+ const months = [
18538
+ "Jan",
18539
+ "Feb",
18540
+ "Mar",
18541
+ "Apr",
18542
+ "May",
18543
+ "Jun",
18544
+ "Jul",
18545
+ "Aug",
18546
+ "Sep",
18547
+ "Oct",
18548
+ "Nov",
18549
+ "Dec"
18550
+ ];
18487
18551
  return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2, " ")}`;
18488
18552
  }
18489
18553
  function makeBar(value, max, width) {
@@ -18512,10 +18576,12 @@ function aggregateLocal(events, days) {
18512
18576
  const estimate = estimateLocalCost(event);
18513
18577
  if (estimate.type === "priced" && estimate.cost > 0) {
18514
18578
  totalCost += estimate.cost;
18515
- if (!serviceCosts[event.service]) serviceCosts[event.service] = { cost: 0, calls: 0 };
18579
+ if (!serviceCosts[event.service])
18580
+ serviceCosts[event.service] = { cost: 0, calls: 0 };
18516
18581
  serviceCosts[event.service].cost += estimate.cost;
18517
18582
  serviceCosts[event.service].calls++;
18518
- if (!dayCosts[eventDateStr]) dayCosts[eventDateStr] = { cost: 0, calls: 0, services: {} };
18583
+ if (!dayCosts[eventDateStr])
18584
+ dayCosts[eventDateStr] = { cost: 0, calls: 0, services: {} };
18519
18585
  dayCosts[eventDateStr].cost += estimate.cost;
18520
18586
  dayCosts[eventDateStr].calls++;
18521
18587
  dayCosts[eventDateStr].services[event.service] = (dayCosts[eventDateStr].services[event.service] ?? 0) + estimate.cost;
@@ -18543,17 +18609,24 @@ function aggregateLocal(events, days) {
18543
18609
  }
18544
18610
  function renderCallCountOnly(data) {
18545
18611
  const maxCalls = data.allServiceCalls.length > 0 ? data.allServiceCalls[0].calls : 0;
18546
- const maxNameLen = Math.max(...data.allServiceCalls.map((s) => s.name.length), 8);
18612
+ const maxNameLen = Math.max(
18613
+ ...data.allServiceCalls.map((s) => s.name.length),
18614
+ 8
18615
+ );
18547
18616
  for (const svc of data.allServiceCalls) {
18548
18617
  const bar = makeBar(svc.calls, maxCalls, 20);
18549
- console.log(` ${svc.name.padEnd(maxNameLen)} ${source_default.gray(`${String(svc.calls).padStart(5)} calls`)} ${source_default.cyan(bar)}`);
18618
+ console.log(
18619
+ ` ${svc.name.padEnd(maxNameLen)} ${source_default.gray(`${String(svc.calls).padStart(5)} calls`)} ${source_default.cyan(bar)}`
18620
+ );
18550
18621
  }
18551
18622
  console.log();
18552
18623
  }
18553
18624
  function renderCostReport(data, label, showDaily, isToday) {
18554
- console.log(`
18625
+ console.log(
18626
+ `
18555
18627
  ${source_default.hex("#FA5D19").bold("burn0 report")} ${source_default.gray(`\u2500\u2500 ${label}`)}
18556
- `);
18628
+ `
18629
+ );
18557
18630
  if (data.total.calls === 0) {
18558
18631
  const msg = isToday ? "No calls today." : `No cost data yet. Run your app with \`import '@burn0/burn0'\` to start tracking.`;
18559
18632
  console.log(source_default.dim(` ${msg}
@@ -18561,50 +18634,70 @@ function renderCostReport(data, label, showDaily, isToday) {
18561
18634
  return;
18562
18635
  }
18563
18636
  if (!data.pricingAvailable) {
18564
- console.log(source_default.dim(` ${data.total.calls} calls tracked (pricing data not available)
18565
- `));
18637
+ console.log(
18638
+ source_default.dim(
18639
+ ` ${data.total.calls} calls tracked (pricing data not available)
18640
+ `
18641
+ )
18642
+ );
18566
18643
  renderCallCountOnly(data);
18567
18644
  return;
18568
18645
  }
18569
18646
  if (data.total.cost === 0 && data.total.calls > 0) {
18570
- console.log(source_default.dim(` ${data.total.calls} calls tracked (no pricing data available)
18571
- `));
18647
+ console.log(
18648
+ source_default.dim(
18649
+ ` ${data.total.calls} calls tracked (no pricing data available)
18650
+ `
18651
+ )
18652
+ );
18572
18653
  renderCallCountOnly(data);
18573
18654
  return;
18574
18655
  }
18575
- console.log(` ${source_default.bold("Total:")} ${source_default.green(formatCost(data.total.cost))} ${source_default.gray(`(${data.total.calls} calls)`)}
18576
- `);
18656
+ console.log(
18657
+ ` ${source_default.bold("Total:")} ${source_default.green(formatCost(data.total.cost))} ${source_default.gray(`(${data.total.calls} calls)`)}
18658
+ `
18659
+ );
18577
18660
  const maxCost = data.byService.length > 0 ? data.byService[0].cost : 0;
18578
18661
  const maxNameLen = Math.max(...data.byService.map((s) => s.name.length), 8);
18579
18662
  for (const svc of data.byService) {
18580
18663
  const pct = data.total.cost > 0 ? Math.round(svc.cost / data.total.cost * 100) : 0;
18581
18664
  const bar = makeBar(svc.cost, maxCost, 20);
18582
- console.log(` ${svc.name.padEnd(maxNameLen)} ${source_default.green(formatCost(svc.cost).padStart(10))} ${source_default.cyan(bar)} ${source_default.gray(`${String(pct).padStart(3)}%`)}`);
18665
+ console.log(
18666
+ ` ${svc.name.padEnd(maxNameLen)} ${source_default.green(formatCost(svc.cost).padStart(10))} ${source_default.cyan(bar)} ${source_default.gray(`${String(pct).padStart(3)}%`)}`
18667
+ );
18583
18668
  }
18584
18669
  if (data.unpricedCount > 0) {
18585
18670
  console.log(source_default.dim(`
18586
18671
  + ${data.unpricedCount} calls not priced`));
18587
18672
  }
18588
18673
  if (showDaily && data.byDay.length > 0) {
18589
- console.log(`
18674
+ console.log(
18675
+ `
18590
18676
  ${source_default.gray("\u2500\u2500 daily \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
18591
- `);
18677
+ `
18678
+ );
18592
18679
  const maxDayCost = Math.max(...data.byDay.map((d) => d.cost));
18593
18680
  for (const day of data.byDay) {
18594
18681
  const dateLabel = formatDateLabel(day.date);
18595
18682
  const bar = makeBar(day.cost, maxDayCost, 12);
18596
18683
  const top2 = day.topServices.slice(0, 2).map((s) => `${s.name} ${formatCost(s.cost)}`).join(" \xB7 ");
18597
18684
  const more = day.topServices.length > 2 ? ` +${day.topServices.length - 2} more` : "";
18598
- console.log(` ${source_default.gray(dateLabel)} ${source_default.green(formatCost(day.cost).padStart(10))} ${source_default.cyan(bar)} ${source_default.dim(top2 + more)}`);
18685
+ console.log(
18686
+ ` ${source_default.gray(dateLabel)} ${source_default.green(formatCost(day.cost).padStart(10))} ${source_default.cyan(bar)} ${source_default.dim(top2 + more)}`
18687
+ );
18599
18688
  }
18600
18689
  }
18601
18690
  if (data.total.cost > 0) {
18602
18691
  const daysInPeriod = showDaily ? 7 : 1;
18603
18692
  const dailyRate = data.total.cost / daysInPeriod;
18604
18693
  const monthly = dailyRate * 30;
18605
- console.log(`
18606
- ${source_default.gray("\u2500\u2500 projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`);
18607
- console.log(` ${source_default.gray("~")}${source_default.green(formatCost(monthly))}${source_default.gray("/mo estimated")} ${source_default.dim(`(based on ${isToday ? "today" : "last 7 days"})`)}`);
18694
+ console.log(
18695
+ `
18696
+ ${source_default.gray("\u2500\u2500 projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`
18697
+ );
18698
+ console.log(
18699
+ ` ${source_default.gray("~")}${source_default.green(formatCost(monthly))}${source_default.gray("/mo estimated")} ${source_default.dim(`(based on ${isToday ? "today" : "last 7 days"})`)}`
18700
+ );
18608
18701
  }
18609
18702
  console.log();
18610
18703
  }
@@ -18612,10 +18705,16 @@ async function fetchBackendReport(apiKey, days) {
18612
18705
  try {
18613
18706
  const controller = new AbortController();
18614
18707
  const timeout = setTimeout(() => controller.abort(), 5e3);
18615
- const response = await globalThis.fetch(`${BURN0_API_URL}/v1/report?days=${days}`, {
18616
- headers: { "Accept": "application/json", "Authorization": `Bearer ${apiKey}` },
18617
- signal: controller.signal
18618
- });
18708
+ const response = await globalThis.fetch(
18709
+ `${BURN0_API_URL}/v1/report?days=${days}`,
18710
+ {
18711
+ headers: {
18712
+ Accept: "application/json",
18713
+ Authorization: `Bearer ${apiKey}`
18714
+ },
18715
+ signal: controller.signal
18716
+ }
18717
+ );
18619
18718
  clearTimeout(timeout);
18620
18719
  if (!response.ok) return null;
18621
18720
  const data = await response.json();
@@ -18623,7 +18722,10 @@ async function fetchBackendReport(apiKey, days) {
18623
18722
  total: data.total ?? { cost: 0, calls: 0 },
18624
18723
  byService: data.byService ?? [],
18625
18724
  byDay: data.byDay ?? [],
18626
- allServiceCalls: (data.byService ?? []).map((s) => ({ name: s.name, calls: s.calls })),
18725
+ allServiceCalls: (data.byService ?? []).map((s) => ({
18726
+ name: s.name,
18727
+ calls: s.calls
18728
+ })),
18627
18729
  unpricedCount: 0,
18628
18730
  pricingAvailable: true
18629
18731
  };
@@ -18657,7 +18759,7 @@ var init_report = __esm({
18657
18759
  init_local();
18658
18760
  init_local_pricing();
18659
18761
  init_env();
18660
- BURN0_API_URL = process.env.BURN0_API_URL ?? "https://burn0-server-production.up.railway.app";
18762
+ BURN0_API_URL = "https://burn0-server-production.up.railway.app";
18661
18763
  }
18662
18764
  });
18663
18765
 
package/dist/index.js CHANGED
@@ -458,32 +458,10 @@ function createRestorer(deps) {
458
458
  // src/transport/dispatcher.ts
459
459
  function createDispatcher(mode2, deps) {
460
460
  return (event) => {
461
- switch (mode2) {
462
- case "dev-local":
463
- deps.logEvent?.(event);
464
- deps.writeLedger?.(event);
465
- break;
466
- case "dev-cloud":
467
- deps.logEvent?.(event);
468
- deps.writeLedger?.(event);
469
- deps.addToBatch?.(event);
470
- break;
471
- case "prod-cloud":
472
- deps.logEvent?.(event);
473
- deps.writeLedger?.(event);
474
- deps.addToBatch?.(event);
475
- break;
476
- case "prod-local":
477
- deps.logEvent?.(event);
478
- break;
479
- case "test-enabled":
480
- deps.logEvent?.(event);
481
- deps.writeLedger?.(event);
482
- deps.addToBatch?.(event);
483
- break;
484
- case "test-disabled":
485
- break;
486
- }
461
+ if (mode2 === "test-disabled") return;
462
+ deps.logEvent?.(event);
463
+ deps.writeLedger?.(event);
464
+ deps.addToBatch?.(event);
487
465
  };
488
466
  }
489
467
 
@@ -529,6 +507,7 @@ var import_node_fs = __toESM(require("fs"));
529
507
  var import_node_path = __toESM(require("path"));
530
508
  var BURN0_DIR = ".burn0";
531
509
  var LEDGER_FILE = "costs.jsonl";
510
+ var SYNC_MARKER_FILE = "last-sync.txt";
532
511
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
533
512
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
534
513
  var LocalLedger = class {
@@ -552,6 +531,24 @@ var LocalLedger = class {
552
531
  return [];
553
532
  }
554
533
  }
534
+ readUnsynced() {
535
+ const all = this.read();
536
+ const lastSync = this.getLastSyncTime();
537
+ if (!lastSync) return all;
538
+ return all.filter((e) => new Date(e.timestamp).getTime() > lastSync);
539
+ }
540
+ markSynced() {
541
+ this.ensureDir();
542
+ import_node_fs.default.writeFileSync(import_node_path.default.join(this.dirPath, SYNC_MARKER_FILE), (/* @__PURE__ */ new Date()).toISOString());
543
+ }
544
+ getLastSyncTime() {
545
+ try {
546
+ const ts = import_node_fs.default.readFileSync(import_node_path.default.join(this.dirPath, SYNC_MARKER_FILE), "utf-8").trim();
547
+ return new Date(ts).getTime();
548
+ } catch {
549
+ return null;
550
+ }
551
+ }
555
552
  ensureDir() {
556
553
  if (!import_node_fs.default.existsSync(this.dirPath)) import_node_fs.default.mkdirSync(this.dirPath, { recursive: true });
557
554
  }
@@ -596,10 +593,17 @@ async function shipEvents(events, apiKey2, baseUrl, fetchFn = globalThis.fetch)
596
593
  },
597
594
  body: JSON.stringify({ events, sdk_version: SDK_VERSION })
598
595
  });
599
- if (response.ok) return true;
596
+ if (response.ok) {
597
+ if (isDebug()) console.log(`[burn0] Shipped ${events.length} events`);
598
+ return true;
599
+ }
600
+ if (isDebug()) {
601
+ const body = await response.text().catch(() => "");
602
+ console.warn(`[burn0] Shipping rejected: ${response.status} ${body}`);
603
+ }
600
604
  } catch (err) {
601
605
  if (isDebug()) {
602
- console.warn("[burn0] Event shipping failed:", err.message);
606
+ console.warn("[burn0] Shipping failed:", err.message);
603
607
  }
604
608
  }
605
609
  if (attempt < maxAttempts - 1) {
@@ -857,31 +861,100 @@ try {
857
861
  }
858
862
  var ticker = createTicker({ todayCost, todayCalls, perServiceCosts });
859
863
  var batch = null;
860
- if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
861
- batch = new BatchBuffer({
864
+ var lateInitDone = false;
865
+ var failedEvents = [];
866
+ function createBatch(key) {
867
+ return new BatchBuffer({
862
868
  sizeThreshold: 50,
863
869
  timeThresholdMs: 1e4,
864
870
  maxSize: 500,
865
871
  onFlush: (events) => {
866
- shipEvents(events, apiKey, BURN0_API_URL, originalFetch2).catch(() => {
872
+ const toShip = failedEvents.length > 0 ? [...failedEvents, ...events] : events;
873
+ failedEvents = [];
874
+ shipEvents(toShip, key, BURN0_API_URL, originalFetch2).then((ok) => {
875
+ if (!ok) {
876
+ failedEvents = toShip.slice(-500);
877
+ }
878
+ }).catch(() => {
879
+ failedEvents = toShip.slice(-500);
867
880
  });
868
881
  }
869
882
  });
870
883
  }
884
+ if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
885
+ batch = createBatch(apiKey);
886
+ }
887
+ var pendingEvents = [];
888
+ function lateInit(event) {
889
+ if (batch) {
890
+ return;
891
+ }
892
+ const lateKey = getApiKey();
893
+ if (!lateKey) {
894
+ if (event) pendingEvents.push(event);
895
+ if (!lateInitDone) {
896
+ lateInitDone = true;
897
+ setTimeout(() => {
898
+ lateInitDone = false;
899
+ if (pendingEvents.length > 0) {
900
+ const e = pendingEvents.shift();
901
+ lateInit(e);
902
+ } else {
903
+ lateInit();
904
+ }
905
+ }, 0);
906
+ }
907
+ return;
908
+ }
909
+ lateInitDone = true;
910
+ apiKey = lateKey;
911
+ mode = detectMode({ isTTY: isTTY(), apiKey });
912
+ batch = createBatch(lateKey);
913
+ fetchPricing(BURN0_API_URL, originalFetch2).catch(() => {
914
+ });
915
+ for (const e of pendingEvents) {
916
+ batch.add(e);
917
+ }
918
+ pendingEvents.length = 0;
919
+ syncLedger(lateKey);
920
+ }
921
+ function syncLedger(key) {
922
+ try {
923
+ const unsynced = ledger.readUnsynced();
924
+ if (unsynced.length === 0) {
925
+ ledger.markSynced();
926
+ return;
927
+ }
928
+ const promises = [];
929
+ for (let i = 0; i < unsynced.length; i += 500) {
930
+ const chunk = unsynced.slice(i, i + 500);
931
+ promises.push(shipEvents(chunk, key, BURN0_API_URL, originalFetch2));
932
+ }
933
+ Promise.all(promises).then((results) => {
934
+ if (results.every(Boolean)) {
935
+ ledger.markSynced();
936
+ }
937
+ }).catch(() => {
938
+ });
939
+ } catch {
940
+ }
941
+ }
871
942
  var shouldWriteLedger = mode !== "test-disabled" && mode !== "prod-local";
872
943
  var dispatch = createDispatcher(mode, {
873
944
  logEvent: (e) => ticker.tick(e),
874
945
  writeLedger: shouldWriteLedger ? (e) => ledger.write(e) : void 0,
875
- addToBatch: batch ? (e) => batch.add(e) : void 0
946
+ addToBatch: (e) => {
947
+ lateInit();
948
+ batch?.add(e);
949
+ }
876
950
  });
877
951
  var preloaded = checkImportOrder();
878
952
  if (preloaded.length > 0) {
879
- console.warn(`[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`);
880
- }
881
- if (mode === "prod-local") {
882
- console.warn("[burn0] No API key \u2014 costs not tracked. Get one free at burn0.dev/api");
953
+ console.warn(
954
+ `[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`
955
+ );
883
956
  }
884
- if (canPatch() && mode !== "test-disabled" && mode !== "prod-local") {
957
+ if (canPatch() && mode !== "test-disabled") {
885
958
  const onEvent = (event) => {
886
959
  const enriched = enrichEvent(event);
887
960
  dispatch(enriched);
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  restore,
3
3
  startSpan,
4
4
  track
5
- } from "./chunk-H3A5NM5C.mjs";
5
+ } from "./chunk-UI6QVWA4.mjs";
6
6
  import "./chunk-DJ72YN4C.mjs";
7
7
  export {
8
8
  restore,
package/dist/register.js CHANGED
@@ -444,32 +444,10 @@ function createRestorer(deps) {
444
444
  // src/transport/dispatcher.ts
445
445
  function createDispatcher(mode2, deps) {
446
446
  return (event) => {
447
- switch (mode2) {
448
- case "dev-local":
449
- deps.logEvent?.(event);
450
- deps.writeLedger?.(event);
451
- break;
452
- case "dev-cloud":
453
- deps.logEvent?.(event);
454
- deps.writeLedger?.(event);
455
- deps.addToBatch?.(event);
456
- break;
457
- case "prod-cloud":
458
- deps.logEvent?.(event);
459
- deps.writeLedger?.(event);
460
- deps.addToBatch?.(event);
461
- break;
462
- case "prod-local":
463
- deps.logEvent?.(event);
464
- break;
465
- case "test-enabled":
466
- deps.logEvent?.(event);
467
- deps.writeLedger?.(event);
468
- deps.addToBatch?.(event);
469
- break;
470
- case "test-disabled":
471
- break;
472
- }
447
+ if (mode2 === "test-disabled") return;
448
+ deps.logEvent?.(event);
449
+ deps.writeLedger?.(event);
450
+ deps.addToBatch?.(event);
473
451
  };
474
452
  }
475
453
 
@@ -515,6 +493,7 @@ var import_node_fs = __toESM(require("fs"));
515
493
  var import_node_path = __toESM(require("path"));
516
494
  var BURN0_DIR = ".burn0";
517
495
  var LEDGER_FILE = "costs.jsonl";
496
+ var SYNC_MARKER_FILE = "last-sync.txt";
518
497
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
519
498
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
520
499
  var LocalLedger = class {
@@ -538,6 +517,24 @@ var LocalLedger = class {
538
517
  return [];
539
518
  }
540
519
  }
520
+ readUnsynced() {
521
+ const all = this.read();
522
+ const lastSync = this.getLastSyncTime();
523
+ if (!lastSync) return all;
524
+ return all.filter((e) => new Date(e.timestamp).getTime() > lastSync);
525
+ }
526
+ markSynced() {
527
+ this.ensureDir();
528
+ import_node_fs.default.writeFileSync(import_node_path.default.join(this.dirPath, SYNC_MARKER_FILE), (/* @__PURE__ */ new Date()).toISOString());
529
+ }
530
+ getLastSyncTime() {
531
+ try {
532
+ const ts = import_node_fs.default.readFileSync(import_node_path.default.join(this.dirPath, SYNC_MARKER_FILE), "utf-8").trim();
533
+ return new Date(ts).getTime();
534
+ } catch {
535
+ return null;
536
+ }
537
+ }
541
538
  ensureDir() {
542
539
  if (!import_node_fs.default.existsSync(this.dirPath)) import_node_fs.default.mkdirSync(this.dirPath, { recursive: true });
543
540
  }
@@ -582,10 +579,17 @@ async function shipEvents(events, apiKey2, baseUrl, fetchFn = globalThis.fetch)
582
579
  },
583
580
  body: JSON.stringify({ events, sdk_version: SDK_VERSION })
584
581
  });
585
- if (response.ok) return true;
582
+ if (response.ok) {
583
+ if (isDebug()) console.log(`[burn0] Shipped ${events.length} events`);
584
+ return true;
585
+ }
586
+ if (isDebug()) {
587
+ const body = await response.text().catch(() => "");
588
+ console.warn(`[burn0] Shipping rejected: ${response.status} ${body}`);
589
+ }
586
590
  } catch (err) {
587
591
  if (isDebug()) {
588
- console.warn("[burn0] Event shipping failed:", err.message);
592
+ console.warn("[burn0] Shipping failed:", err.message);
589
593
  }
590
594
  }
591
595
  if (attempt < maxAttempts - 1) {
@@ -843,31 +847,100 @@ try {
843
847
  }
844
848
  var ticker = createTicker({ todayCost, todayCalls, perServiceCosts });
845
849
  var batch = null;
846
- if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
847
- batch = new BatchBuffer({
850
+ var lateInitDone = false;
851
+ var failedEvents = [];
852
+ function createBatch(key) {
853
+ return new BatchBuffer({
848
854
  sizeThreshold: 50,
849
855
  timeThresholdMs: 1e4,
850
856
  maxSize: 500,
851
857
  onFlush: (events) => {
852
- shipEvents(events, apiKey, BURN0_API_URL, originalFetch2).catch(() => {
858
+ const toShip = failedEvents.length > 0 ? [...failedEvents, ...events] : events;
859
+ failedEvents = [];
860
+ shipEvents(toShip, key, BURN0_API_URL, originalFetch2).then((ok) => {
861
+ if (!ok) {
862
+ failedEvents = toShip.slice(-500);
863
+ }
864
+ }).catch(() => {
865
+ failedEvents = toShip.slice(-500);
853
866
  });
854
867
  }
855
868
  });
856
869
  }
870
+ if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
871
+ batch = createBatch(apiKey);
872
+ }
873
+ var pendingEvents = [];
874
+ function lateInit(event) {
875
+ if (batch) {
876
+ return;
877
+ }
878
+ const lateKey = getApiKey();
879
+ if (!lateKey) {
880
+ if (event) pendingEvents.push(event);
881
+ if (!lateInitDone) {
882
+ lateInitDone = true;
883
+ setTimeout(() => {
884
+ lateInitDone = false;
885
+ if (pendingEvents.length > 0) {
886
+ const e = pendingEvents.shift();
887
+ lateInit(e);
888
+ } else {
889
+ lateInit();
890
+ }
891
+ }, 0);
892
+ }
893
+ return;
894
+ }
895
+ lateInitDone = true;
896
+ apiKey = lateKey;
897
+ mode = detectMode({ isTTY: isTTY(), apiKey });
898
+ batch = createBatch(lateKey);
899
+ fetchPricing(BURN0_API_URL, originalFetch2).catch(() => {
900
+ });
901
+ for (const e of pendingEvents) {
902
+ batch.add(e);
903
+ }
904
+ pendingEvents.length = 0;
905
+ syncLedger(lateKey);
906
+ }
907
+ function syncLedger(key) {
908
+ try {
909
+ const unsynced = ledger.readUnsynced();
910
+ if (unsynced.length === 0) {
911
+ ledger.markSynced();
912
+ return;
913
+ }
914
+ const promises = [];
915
+ for (let i = 0; i < unsynced.length; i += 500) {
916
+ const chunk = unsynced.slice(i, i + 500);
917
+ promises.push(shipEvents(chunk, key, BURN0_API_URL, originalFetch2));
918
+ }
919
+ Promise.all(promises).then((results) => {
920
+ if (results.every(Boolean)) {
921
+ ledger.markSynced();
922
+ }
923
+ }).catch(() => {
924
+ });
925
+ } catch {
926
+ }
927
+ }
857
928
  var shouldWriteLedger = mode !== "test-disabled" && mode !== "prod-local";
858
929
  var dispatch = createDispatcher(mode, {
859
930
  logEvent: (e) => ticker.tick(e),
860
931
  writeLedger: shouldWriteLedger ? (e) => ledger.write(e) : void 0,
861
- addToBatch: batch ? (e) => batch.add(e) : void 0
932
+ addToBatch: (e) => {
933
+ lateInit();
934
+ batch?.add(e);
935
+ }
862
936
  });
863
937
  var preloaded = checkImportOrder();
864
938
  if (preloaded.length > 0) {
865
- console.warn(`[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`);
866
- }
867
- if (mode === "prod-local") {
868
- console.warn("[burn0] No API key \u2014 costs not tracked. Get one free at burn0.dev/api");
939
+ console.warn(
940
+ `[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`
941
+ );
869
942
  }
870
- if (canPatch() && mode !== "test-disabled" && mode !== "prod-local") {
943
+ if (canPatch() && mode !== "test-disabled") {
871
944
  const onEvent = (event) => {
872
945
  const enriched = enrichEvent(event);
873
946
  dispatch(enriched);
package/dist/register.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import "./chunk-H3A5NM5C.mjs";
1
+ import "./chunk-UI6QVWA4.mjs";
2
2
  import "./chunk-DJ72YN4C.mjs";
3
3
  //# sourceMappingURL=register.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burn0/burn0",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Lightweight cost observability for every API call in your stack",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",