@burn0/burn0 0.1.0 โ 0.2.1
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 +341 -2
- package/dist/{chunk-ZHAS7BCI.mjs โ chunk-KKYHE4ZV.mjs} +146 -125
- package/dist/cli/index.js +528 -269
- package/dist/index.js +145 -124
- package/dist/index.mjs +1 -1
- package/dist/register.js +145 -124
- package/dist/register.mjs +1 -1
- package/package.json +20 -3
- package/scripts/postinstall.js +27 -5
- package/dist/chunk-DJ72YN4C.mjs.map +0 -1
- package/dist/chunk-ZHAS7BCI.mjs.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/register.js.map +0 -1
- package/dist/register.mjs.map +0 -1
- package/dist/track.js.map +0 -1
- package/dist/track.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,2 +1,341 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<h1>๐ฅ burn0</h1>
|
|
4
|
+
|
|
5
|
+
<h3>Know what your code costs</h3>
|
|
6
|
+
|
|
7
|
+
<p>One import tracks every API call in your stack.<br>
|
|
8
|
+
LLMs, SaaS, infrastructure. See per-request costs in real time.<br><br>
|
|
9
|
+
<strong>The cost observability layer your codebase is missing.</strong></p>
|
|
10
|
+
|
|
11
|
+
<br>
|
|
12
|
+
|
|
13
|
+
<img src="https://img.shields.io/badge/๐ฅ_One_Import-black?style=for-the-badge" alt="One import">
|
|
14
|
+
<img src="https://img.shields.io/badge/๐_50+_Services-blue?style=for-the-badge" alt="50+ services">
|
|
15
|
+
<img src="https://img.shields.io/badge/โก_Sub--ms_Overhead-yellow?style=for-the-badge" alt="Sub-ms overhead">
|
|
16
|
+
<img src="https://img.shields.io/badge/๐_MIT_Licensed-green?style=for-the-badge" alt="MIT licensed">
|
|
17
|
+
|
|
18
|
+
[](https://npmjs.com/package/@burn0/burn0)
|
|
19
|
+
[](https://npmjs.com/package/@burn0/burn0)
|
|
20
|
+
[](https://github.com/burn0-dev/burn0)
|
|
21
|
+
[](https://typescriptlang.org)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
|
|
24
|
+
<br>
|
|
25
|
+
|
|
26
|
+
[Website](https://burn0.dev) ยท [Docs](https://docs.burn0.dev) ยท [Dashboard](https://burn0.dev/dashboard) ยท [Twitter](https://twitter.com/burn0dev)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## The Problem
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
**You have no idea which feature is burning money.**
|
|
37
|
+
|
|
38
|
+
Observability platforms charge $199/mo. API monitoring tools charge $149/mo. Cost management SaaS charges $99/mo.
|
|
39
|
+
|
|
40
|
+
**burn0 is free. One import. That's it.**
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
burn0 โธ $4.32 today (47 calls) โโ openai: $3.80 ยท anthropic: $0.52
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm i @burn0/burn0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Add one line to your entry file:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import '@burn0/burn0' // Must be first import
|
|
58
|
+
|
|
59
|
+
import express from 'express'
|
|
60
|
+
import OpenAI from 'openai'
|
|
61
|
+
// ... your app runs exactly the same
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's it. Costs appear in your terminal:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
burn0 โธ $0.47 today (12 calls) โโ openai: $0.41 ยท stripe: $0.06
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
On exit:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
burn0 โธ session: $0.47 (12 calls, 4m 22s) โโ today: $14.32 โโ ~$430/mo
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## How It Compares
|
|
79
|
+
|
|
80
|
+
| | Observability Platform | API Monitoring | Cost Management SaaS | **burn0** |
|
|
81
|
+
| ------------------- | ---------------------- | -------------- | -------------------- | ---------------------- |
|
|
82
|
+
| **Price** | $199/mo | $149/mo | $99/mo | **Free forever** |
|
|
83
|
+
| **Setup** | SDK + dashboard config | Proxy setup | Manual tagging | **One import** |
|
|
84
|
+
| **Latency** | 5-50ms | 10-100ms | Async | **<1ms** |
|
|
85
|
+
| **Per-feature** | Manual instrumentation | No | Manual | **`burn0.track()`** |
|
|
86
|
+
| **Works locally** | No | No | No | **Yes** |
|
|
87
|
+
| **Open source** | No | No | No | **MIT Licensed** |
|
|
88
|
+
| **Data leaves app** | Always | Always | Always | **Only if you opt in** |
|
|
89
|
+
|
|
90
|
+
**You're spending $526/mo on tools that burn0 replaces for $0.**
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## CLI
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Interactive setup wizard
|
|
98
|
+
npx burn0 init
|
|
99
|
+
|
|
100
|
+
# Cost report (last 7 days)
|
|
101
|
+
npx burn0 report
|
|
102
|
+
|
|
103
|
+
# Today only
|
|
104
|
+
npx burn0 report --today
|
|
105
|
+
|
|
106
|
+
# Run any app with tracking (zero code changes)
|
|
107
|
+
npx burn0 dev -- node app.js
|
|
108
|
+
|
|
109
|
+
# Connect to cloud dashboard
|
|
110
|
+
npx burn0 connect
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `burn0 report` output
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
burn0 report โโ last 7 days
|
|
117
|
+
|
|
118
|
+
Total: $12.47 (342 calls)
|
|
119
|
+
|
|
120
|
+
openai $8.32 โโโโโโโโโโโโโโโโโโโโ 67%
|
|
121
|
+
anthropic $3.15 โโโโโโโโโโโโโโโโโโโโ 25%
|
|
122
|
+
google-gemini $0.85 โโโโโโโโโโโโโโโโโโโโ 7%
|
|
123
|
+
resend $0.15 โโโโโโโโโโโโโโโโโโโโ 1%
|
|
124
|
+
|
|
125
|
+
โโ projection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
126
|
+
~$53/mo estimated (based on last 7 days)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Feature Attribution
|
|
132
|
+
|
|
133
|
+
Know exactly which feature burns money:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { track } from '@burn0/burn0'
|
|
137
|
+
|
|
138
|
+
await track('onboarding', async () => {
|
|
139
|
+
const profile = await ai.generateProfile(user)
|
|
140
|
+
await stripe.createSubscription(user.id)
|
|
141
|
+
await sendWelcomeEmail(user.email)
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
burn0 โธ feature "onboarding" โโ $0.47/user
|
|
147
|
+
โโ openai $0.39 (83%)
|
|
148
|
+
โโ stripe $0.0001 (2%)
|
|
149
|
+
โโ sendgrid $0.08 (17%)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Track per user, per feature, per request. No manual tagging. No dashboards to configure.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 50+ Services Supported
|
|
157
|
+
|
|
158
|
+
burn0 auto-detects services from hostnames. Zero configuration.
|
|
159
|
+
|
|
160
|
+
### AI / LLMs
|
|
161
|
+
|
|
162
|
+
| Service | Detection | Pricing Model |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| OpenAI | `api.openai.com` | Per-token (exact) |
|
|
165
|
+
| Anthropic | `api.anthropic.com` | Per-token (exact) |
|
|
166
|
+
| Google Gemini | `generativelanguage.googleapis.com` | Per-token (exact) |
|
|
167
|
+
| Mistral | `api.mistral.ai` | Per-token (exact) |
|
|
168
|
+
| Cohere | `api.cohere.ai` | Per-token |
|
|
169
|
+
| Groq | `api.groq.com` | Per-token |
|
|
170
|
+
| Together AI | `api.together.xyz` | Per-token |
|
|
171
|
+
| Perplexity | `api.perplexity.ai` | Per-token |
|
|
172
|
+
| DeepSeek | `api.deepseek.com` | Per-token |
|
|
173
|
+
| Replicate | `api.replicate.com` | Per-second |
|
|
174
|
+
| Fireworks AI | `api.fireworks.ai` | Per-token |
|
|
175
|
+
| AI21 Labs | `api.ai21.com` | Per-token |
|
|
176
|
+
| Pinecone | `*.pinecone.io` | Per-request |
|
|
177
|
+
|
|
178
|
+
### Pay-per-use APIs
|
|
179
|
+
|
|
180
|
+
| Service | Detection | Pricing Model |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| Stripe | `api.stripe.com` | Per-transaction |
|
|
183
|
+
| PayPal | `api.paypal.com` | Per-transaction |
|
|
184
|
+
| Plaid | `*.plaid.com` | Per-request |
|
|
185
|
+
| SendGrid | `api.sendgrid.com` | Per-email |
|
|
186
|
+
| Resend | `api.resend.com` | Per-email |
|
|
187
|
+
| Twilio | `api.twilio.com` | Per-message |
|
|
188
|
+
| Vonage | `api.nexmo.com` | Per-message |
|
|
189
|
+
| Algolia | `*.algolia.net` | Per-search |
|
|
190
|
+
| Google Maps | `maps.googleapis.com` | Per-request |
|
|
191
|
+
| Mapbox | `api.mapbox.com` | Per-request |
|
|
192
|
+
| Cloudinary | `api.cloudinary.com` | Per-transform |
|
|
193
|
+
| Sentry | `sentry.io` | Per-event |
|
|
194
|
+
| Segment | `api.segment.io` | Per-event |
|
|
195
|
+
| Mixpanel | `api.mixpanel.com` | Per-event |
|
|
196
|
+
|
|
197
|
+
### Databases & Infrastructure
|
|
198
|
+
|
|
199
|
+
| Service | Detection | Pricing Model |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| Supabase | `*.supabase.co` | Per-request |
|
|
202
|
+
| PlanetScale | `*.psdb.cloud` | Per-request |
|
|
203
|
+
| MongoDB Atlas | `*.mongodb.net` | Per-request |
|
|
204
|
+
| Upstash | `*.upstash.io` | Per-request |
|
|
205
|
+
| Neon | `*.neon.tech` | Per-request |
|
|
206
|
+
| Turso | `*.turso.io` | Per-request |
|
|
207
|
+
| Firebase | `*.firebaseio.com` | Per-request |
|
|
208
|
+
| AWS S3 | `*.s3.amazonaws.com` | Per-request |
|
|
209
|
+
| AWS Lambda | `lambda.*.amazonaws.com` | Per-invocation |
|
|
210
|
+
| Vercel | `api.vercel.com` | Per-request |
|
|
211
|
+
|
|
212
|
+
**Unknown APIs are auto-tracked by request count.** Nothing slips through.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## How It Works
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
Your app starts
|
|
220
|
+
โ
|
|
221
|
+
โโ import '@burn0/burn0' patches globalThis.fetch + node:http
|
|
222
|
+
โ
|
|
223
|
+
โโ Every outbound HTTP call is intercepted (zero behavior change)
|
|
224
|
+
โ
|
|
225
|
+
โโ Service identified from hostname (api.openai.com โ OpenAI)
|
|
226
|
+
โ
|
|
227
|
+
โโ Token counts + costs extracted from response metadata
|
|
228
|
+
โ
|
|
229
|
+
โโ Costs displayed in terminal + stored in local ledger
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
1. **Interception is synchronous** โ your request goes out immediately
|
|
233
|
+
2. **Cost extraction is async** โ happens after the response, never blocks
|
|
234
|
+
3. **Sub-millisecond overhead** โ benchmarked, not estimated
|
|
235
|
+
4. **Never reads content** โ only extracts metadata: service, model, tokens, status, latency
|
|
236
|
+
5. **Never throws** โ graceful degradation if anything fails internally
|
|
237
|
+
6. **ยฑ2% accuracy** โ exact token counts from LLM APIs, bundled pricing for SaaS
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Two Modes
|
|
242
|
+
|
|
243
|
+
| Mode | API Key | What happens |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| **Local** (default) | No | Costs in terminal + local ledger. Zero network calls to burn0. |
|
|
246
|
+
| **Cloud** (opt-in) | Yes | Same as local + events sync to dashboard for team visibility. |
|
|
247
|
+
|
|
248
|
+
### Cloud Dashboard
|
|
249
|
+
|
|
250
|
+
Connect an API key to see costs in the browser:
|
|
251
|
+
|
|
252
|
+
- **Live event feed** โ every API call in real-time via SSE
|
|
253
|
+
- **Cost breakdown** โ per service, per model, per day
|
|
254
|
+
- **Monthly projection** โ estimated monthly spend based on trends
|
|
255
|
+
- **API key management** โ create, list, revoke keys
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Sign in with GitHub at burn0.dev
|
|
259
|
+
# Create an API key, then:
|
|
260
|
+
npx burn0 connect
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Configuration
|
|
266
|
+
|
|
267
|
+
| Env Variable | Default | Description |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `BURN0_API_KEY` | โ | API key for cloud mode |
|
|
270
|
+
| `BURN0_API_URL` | `https://api.burn0.dev` | Backend URL |
|
|
271
|
+
| `BURN0_DEBUG` | `false` | Enable debug logging |
|
|
272
|
+
| `BURN0_ENABLE_TEST` | โ | Set to `1` to enable in `NODE_ENV=test` |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Works With Everything
|
|
277
|
+
|
|
278
|
+
burn0 works with any Node.js framework. If it makes HTTP calls, burn0 tracks the costs.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
Express ยท Next.js ยท Fastify ยท Hono ยท Koa ยท NestJS ยท Remix ยท Nuxt
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Requirements:** Node.js >= 18
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Frequently Asked Questions
|
|
289
|
+
|
|
290
|
+
### Does it slow down my API calls?
|
|
291
|
+
|
|
292
|
+
No. Interception is synchronous but event processing is fully async. burn0 adds sub-millisecond overhead to your API calls.
|
|
293
|
+
|
|
294
|
+
### Does it send my data anywhere?
|
|
295
|
+
|
|
296
|
+
By default, no. In local mode, costs are logged to your terminal and stored in a local file. Cloud mode (opt-in) ships only metadata โ never request/response bodies.
|
|
297
|
+
|
|
298
|
+
### How accurate are the cost estimates?
|
|
299
|
+
|
|
300
|
+
burn0 extracts exact token counts from LLM API responses. For pay-per-use APIs, it uses bundled pricing data. Accuracy is within ยฑ2%.
|
|
301
|
+
|
|
302
|
+
### Can I use it in production?
|
|
303
|
+
|
|
304
|
+
Yes. burn0 is designed for production use. It never throws, never adds latency, and gracefully degrades if anything fails internally.
|
|
305
|
+
|
|
306
|
+
### Is it really free?
|
|
307
|
+
|
|
308
|
+
Yes. burn0 is MIT licensed and free forever. No API key required for local mode. Cloud features (dashboard, team analytics) are available as a paid tier.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Development
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
git clone https://github.com/burn0-dev/burn0.git
|
|
316
|
+
cd burn0
|
|
317
|
+
npm install
|
|
318
|
+
npm run build
|
|
319
|
+
npm test
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Community
|
|
325
|
+
|
|
326
|
+
| Channel | Link |
|
|
327
|
+
|---|---|
|
|
328
|
+
| ๐ Website | [burn0.dev](https://burn0.dev) |
|
|
329
|
+
| ๐ Docs | [docs.burn0.dev](https://docs.burn0.dev) |
|
|
330
|
+
| ๐ฆ Twitter | [@burn0dev](https://twitter.com/burn0dev) |
|
|
331
|
+
| ๐ป GitHub | [burn0-dev/burn0](https://github.com/burn0-dev/burn0) |
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
<div align="center">
|
|
336
|
+
|
|
337
|
+
**MIT License** ยท Built by the [burn0](https://burn0.dev) team
|
|
338
|
+
|
|
339
|
+
โญ If burn0 saves you money, consider starring the repo.
|
|
340
|
+
|
|
341
|
+
</div>
|
|
@@ -404,13 +404,16 @@ function createDispatcher(mode2, deps) {
|
|
|
404
404
|
break;
|
|
405
405
|
case "dev-cloud":
|
|
406
406
|
deps.logEvent?.(event);
|
|
407
|
+
deps.writeLedger?.(event);
|
|
407
408
|
deps.addToBatch?.(event);
|
|
408
409
|
break;
|
|
409
410
|
case "prod-cloud":
|
|
411
|
+
deps.logEvent?.(event);
|
|
412
|
+
deps.writeLedger?.(event);
|
|
410
413
|
deps.addToBatch?.(event);
|
|
411
414
|
break;
|
|
412
415
|
case "prod-local":
|
|
413
|
-
deps.
|
|
416
|
+
deps.logEvent?.(event);
|
|
414
417
|
break;
|
|
415
418
|
case "test-enabled":
|
|
416
419
|
deps.logEvent?.(event);
|
|
@@ -641,114 +644,114 @@ function estimateLocalCost(event) {
|
|
|
641
644
|
}
|
|
642
645
|
|
|
643
646
|
// src/transport/logger.ts
|
|
644
|
-
var DIM = "\x1B[2m";
|
|
645
647
|
var RESET = "\x1B[0m";
|
|
646
|
-
var CYAN = "\x1B[36m";
|
|
647
648
|
var GREEN = "\x1B[32m";
|
|
648
|
-
var YELLOW = "\x1B[33m";
|
|
649
|
-
var WHITE = "\x1B[37m";
|
|
650
649
|
var BOLD = "\x1B[1m";
|
|
651
650
|
var ORANGE = "\x1B[38;2;250;93;25m";
|
|
652
651
|
var GRAY = "\x1B[90m";
|
|
653
|
-
var headerPrinted = false;
|
|
654
|
-
var sessionTotal = 0;
|
|
655
|
-
var eventCount = 0;
|
|
656
|
-
function formatTokens(count) {
|
|
657
|
-
if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
|
|
658
|
-
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}K`;
|
|
659
|
-
return count.toString();
|
|
660
|
-
}
|
|
661
652
|
function formatCost(cost) {
|
|
662
653
|
if (cost >= 1) return `$${cost.toFixed(2)}`;
|
|
663
654
|
if (cost >= 0.01) return `$${cost.toFixed(4)}`;
|
|
664
655
|
return `$${cost.toFixed(6)}`;
|
|
665
656
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
`);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
657
|
+
function formatDuration(ms) {
|
|
658
|
+
const seconds = Math.floor(ms / 1e3);
|
|
659
|
+
if (seconds < 60) return `${seconds}s`;
|
|
660
|
+
const minutes = Math.floor(seconds / 60);
|
|
661
|
+
const remainingSeconds = seconds % 60;
|
|
662
|
+
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
|
663
|
+
const hours = Math.floor(minutes / 60);
|
|
664
|
+
const remainingMinutes = minutes % 60;
|
|
665
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
666
|
+
}
|
|
667
|
+
function formatServiceBreakdown(perServiceCosts2, maxWidth) {
|
|
668
|
+
const sorted = Object.entries(perServiceCosts2).filter(([, cost]) => cost > 0).sort((a, b) => b[1] - a[1]);
|
|
669
|
+
if (sorted.length === 0) return "";
|
|
670
|
+
const parts = [];
|
|
671
|
+
let currentWidth = 0;
|
|
672
|
+
let shown = 0;
|
|
673
|
+
for (let i = 0; i < sorted.length && shown < 3; i++) {
|
|
674
|
+
const [name, cost] = sorted[i];
|
|
675
|
+
const part = `${name}: ${formatCost(cost)}`;
|
|
676
|
+
if (currentWidth + part.length + 3 > maxWidth && shown > 0) {
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
parts.push(part);
|
|
680
|
+
currentWidth += part.length + 3;
|
|
681
|
+
shown++;
|
|
682
|
+
}
|
|
683
|
+
const remaining = sorted.length - shown;
|
|
684
|
+
if (remaining > 0) {
|
|
685
|
+
parts.push(`+${remaining} more`);
|
|
686
|
+
}
|
|
687
|
+
return parts.join(" \xB7 ");
|
|
688
|
+
}
|
|
689
|
+
function createTicker(init) {
|
|
690
|
+
let sessionCost = 0;
|
|
691
|
+
let sessionCalls = 0;
|
|
692
|
+
const sessionStartTime = Date.now();
|
|
693
|
+
let todayCost2 = init.todayCost;
|
|
694
|
+
let todayCalls2 = init.todayCalls;
|
|
695
|
+
const perServiceCosts2 = { ...init.perServiceCosts };
|
|
696
|
+
let exitPrinted = false;
|
|
697
|
+
let pricedCalls = 0;
|
|
698
|
+
let lastLineLen = 0;
|
|
699
|
+
function render() {
|
|
700
|
+
if (!process.stderr.isTTY) return;
|
|
701
|
+
if (todayCalls2 === 0) return;
|
|
702
|
+
let content;
|
|
703
|
+
if (pricedCalls === 0 && todayCost2 === 0) {
|
|
704
|
+
content = ` burn0 \u25B8 ${todayCalls2} calls today`;
|
|
705
|
+
} else {
|
|
706
|
+
const breakdown = formatServiceBreakdown(perServiceCosts2, 40);
|
|
707
|
+
const breakdownPart = breakdown ? ` \u2500\u2500 ${breakdown}` : "";
|
|
708
|
+
content = ` burn0 \u25B8 ${formatCost(todayCost2)} today (${todayCalls2} calls)${breakdownPart}`;
|
|
709
|
+
}
|
|
710
|
+
const pad = lastLineLen > content.length ? " ".repeat(lastLineLen - content.length) : "";
|
|
711
|
+
lastLineLen = content.length;
|
|
712
|
+
let colored;
|
|
713
|
+
if (pricedCalls === 0 && todayCost2 === 0) {
|
|
714
|
+
colored = ` ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}${todayCalls2} calls today${RESET}`;
|
|
715
|
+
} else {
|
|
716
|
+
const breakdown = formatServiceBreakdown(perServiceCosts2, 40);
|
|
717
|
+
const breakdownPart = breakdown ? ` ${GRAY}\u2500\u2500${RESET} ${breakdown}` : "";
|
|
718
|
+
colored = ` ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GREEN}${formatCost(todayCost2)}${RESET} ${GRAY}today (${todayCalls2} calls)${RESET}${breakdownPart}`;
|
|
719
|
+
}
|
|
720
|
+
process.stderr.write(`\r${colored}${pad}`);
|
|
721
|
+
}
|
|
722
|
+
function tick(event) {
|
|
723
|
+
const estimate = estimateLocalCost(event);
|
|
724
|
+
todayCalls2++;
|
|
725
|
+
sessionCalls++;
|
|
726
|
+
if (estimate.type === "priced" && estimate.cost > 0) {
|
|
727
|
+
todayCost2 += estimate.cost;
|
|
728
|
+
sessionCost += estimate.cost;
|
|
729
|
+
pricedCalls++;
|
|
730
|
+
perServiceCosts2[event.service] = (perServiceCosts2[event.service] ?? 0) + estimate.cost;
|
|
731
|
+
}
|
|
732
|
+
render();
|
|
733
|
+
}
|
|
734
|
+
function printExitSummary() {
|
|
735
|
+
if (!process.stderr.isTTY) return;
|
|
736
|
+
if (sessionCalls === 0) return;
|
|
737
|
+
if (exitPrinted) return;
|
|
738
|
+
exitPrinted = true;
|
|
739
|
+
const duration = formatDuration(Date.now() - sessionStartTime);
|
|
740
|
+
let line;
|
|
741
|
+
if (pricedCalls === 0 && sessionCost === 0) {
|
|
742
|
+
line = `
|
|
743
|
+
${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}session: ${sessionCalls} calls (${duration})${RESET} ${GRAY}\u2500\u2500${RESET} ${GRAY}today: ${todayCalls2} calls${RESET}
|
|
744
|
+
`;
|
|
745
|
+
} else {
|
|
746
|
+
const monthlyEst = todayCost2 > 0 ? formatCost(todayCost2 * 30) : null;
|
|
747
|
+
const projPart = monthlyEst ? ` ${GRAY}\u2500\u2500${RESET} ${GRAY}~${GREEN}${monthlyEst}${RESET}${GRAY}/mo${RESET}` : "";
|
|
748
|
+
line = `
|
|
749
|
+
${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}session:${RESET} ${GREEN}${formatCost(sessionCost)}${RESET} ${GRAY}(${sessionCalls} calls, ${duration})${RESET} ${GRAY}\u2500\u2500${RESET} ${GRAY}today:${RESET} ${GREEN}${formatCost(todayCost2)}${RESET}${projPart}
|
|
750
|
+
`;
|
|
751
|
+
}
|
|
752
|
+
process.stderr.write(line);
|
|
751
753
|
}
|
|
754
|
+
return { tick, printExitSummary };
|
|
752
755
|
}
|
|
753
756
|
|
|
754
757
|
// src/index.ts
|
|
@@ -757,12 +760,36 @@ var apiKey = getApiKey();
|
|
|
757
760
|
var mode = detectMode({ isTTY: isTTY(), apiKey });
|
|
758
761
|
var { track, startSpan, enrichEvent } = createTracker();
|
|
759
762
|
var originalFetch2 = globalThis.fetch;
|
|
760
|
-
if (mode !== "test-disabled") {
|
|
763
|
+
if (mode !== "test-disabled" && mode !== "prod-local") {
|
|
761
764
|
fetchPricing(BURN0_API_URL, originalFetch2).catch(() => {
|
|
762
765
|
});
|
|
763
766
|
}
|
|
764
|
-
var
|
|
765
|
-
|
|
767
|
+
var ledger = new LocalLedger(process.cwd());
|
|
768
|
+
function getTodayDateStr() {
|
|
769
|
+
const d = /* @__PURE__ */ new Date();
|
|
770
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
771
|
+
}
|
|
772
|
+
var todayCost = 0;
|
|
773
|
+
var todayCalls = 0;
|
|
774
|
+
var perServiceCosts = {};
|
|
775
|
+
try {
|
|
776
|
+
const todayStr = getTodayDateStr();
|
|
777
|
+
const allEvents = ledger.read();
|
|
778
|
+
for (const event of allEvents) {
|
|
779
|
+
const eventDate = new Date(event.timestamp);
|
|
780
|
+
const eventDateStr = `${eventDate.getFullYear()}-${String(eventDate.getMonth() + 1).padStart(2, "0")}-${String(eventDate.getDate()).padStart(2, "0")}`;
|
|
781
|
+
if (eventDateStr === todayStr) {
|
|
782
|
+
todayCalls++;
|
|
783
|
+
const estimate = estimateLocalCost(event);
|
|
784
|
+
if (estimate.type === "priced" && estimate.cost > 0) {
|
|
785
|
+
todayCost += estimate.cost;
|
|
786
|
+
perServiceCosts[event.service] = (perServiceCosts[event.service] ?? 0) + estimate.cost;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
var ticker = createTicker({ todayCost, todayCalls, perServiceCosts });
|
|
766
793
|
var batch = null;
|
|
767
794
|
if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
|
|
768
795
|
batch = new BatchBuffer({
|
|
@@ -775,17 +802,20 @@ if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
|
|
|
775
802
|
}
|
|
776
803
|
});
|
|
777
804
|
}
|
|
805
|
+
var shouldWriteLedger = mode !== "test-disabled" && mode !== "prod-local";
|
|
778
806
|
var dispatch = createDispatcher(mode, {
|
|
779
|
-
logEvent,
|
|
780
|
-
writeLedger:
|
|
781
|
-
addToBatch: batch ? (e) => batch.add(e) : void 0
|
|
782
|
-
accumulate: (e) => accumulatedEvents.push(e)
|
|
807
|
+
logEvent: (e) => ticker.tick(e),
|
|
808
|
+
writeLedger: shouldWriteLedger ? (e) => ledger.write(e) : void 0,
|
|
809
|
+
addToBatch: batch ? (e) => batch.add(e) : void 0
|
|
783
810
|
});
|
|
784
811
|
var preloaded = checkImportOrder();
|
|
785
812
|
if (preloaded.length > 0) {
|
|
786
|
-
console.warn(`[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import 'burn0'\` to the top of your entry file.`);
|
|
813
|
+
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.`);
|
|
787
814
|
}
|
|
788
|
-
if (
|
|
815
|
+
if (mode === "prod-local") {
|
|
816
|
+
console.warn("[burn0] No API key \u2014 costs not tracked. Get one free at burn0.dev/api");
|
|
817
|
+
}
|
|
818
|
+
if (canPatch() && mode !== "test-disabled" && mode !== "prod-local") {
|
|
789
819
|
const onEvent = (event) => {
|
|
790
820
|
const enriched = enrichEvent(event);
|
|
791
821
|
dispatch(enriched);
|
|
@@ -794,25 +824,16 @@ if (canPatch() && mode !== "test-disabled") {
|
|
|
794
824
|
patchHttp(onEvent);
|
|
795
825
|
markPatched();
|
|
796
826
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
console.log(formatProcessSummary(accumulatedEvents, uptimeSeconds));
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
if (batch) {
|
|
807
|
-
const exitFlush = () => {
|
|
827
|
+
var exitHandled = false;
|
|
828
|
+
process.on("exit", () => {
|
|
829
|
+
if (exitHandled) return;
|
|
830
|
+
exitHandled = true;
|
|
831
|
+
if (batch) {
|
|
808
832
|
batch.flush();
|
|
809
833
|
batch.destroy();
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
process.on("SIGINT", exitFlush);
|
|
814
|
-
process.on("SIGHUP", exitFlush);
|
|
815
|
-
}
|
|
834
|
+
}
|
|
835
|
+
ticker.printExitSummary();
|
|
836
|
+
});
|
|
816
837
|
var restore = createRestorer({ unpatchFetch, unpatchHttp, resetGuard });
|
|
817
838
|
|
|
818
839
|
export {
|
|
@@ -820,4 +841,4 @@ export {
|
|
|
820
841
|
startSpan,
|
|
821
842
|
restore
|
|
822
843
|
};
|
|
823
|
-
//# sourceMappingURL=chunk-
|
|
844
|
+
//# sourceMappingURL=chunk-KKYHE4ZV.mjs.map
|