measure-fn 3.6.0 → 3.7.0
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 +133 -100
- package/index.test.ts +9 -10
- package/index.ts +13 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,154 +2,175 @@
|
|
|
2
2
|
<img src="banner.png" alt="measure-fn" width="100%" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<b>Replace try-catch + timing boilerplate in TypeScript with a single line of code.</b>
|
|
7
|
+
</p>
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/measure-fn"><img src="https://img.shields.io/npm/v/measure-fn.svg" alt="npm version"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/measure-fn"><img src="https://img.shields.io/npm/dm/measure-fn.svg" alt="npm downloads"></a>
|
|
12
|
+
<a href="https://github.com/7flash/measure-fn/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
Whenever a function needs error handling so it doesn't crash, and timing so you know how long it took, you usually end up adding this boilerplate manually:
|
|
16
|
+
|
|
17
|
+
**Before:**
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
let users = null;
|
|
21
|
+
try {
|
|
22
|
+
const start = performance.now();
|
|
23
|
+
users = await fetchUsers();
|
|
24
|
+
const ms = (performance.now() - start).toFixed(2);
|
|
25
|
+
console.log(`[a] ··········· ${ms}ms → ${JSON.stringify(users)}`);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.log(`[a] ✗ Fetch users (${e.message})`);
|
|
28
|
+
console.error(e.stack);
|
|
29
|
+
}
|
|
9
30
|
```
|
|
10
31
|
|
|
32
|
+
**After:** measure-fn does the exact same thing in one line. Completely type-safe (infers `T | null`) and never crashes.
|
|
33
|
+
|
|
11
34
|
```typescript
|
|
12
|
-
import { measure
|
|
35
|
+
import { measure } from 'measure-fn';
|
|
13
36
|
|
|
14
37
|
const users = await measure('Fetch users', () => fetchUsers());
|
|
15
|
-
|
|
38
|
+
// → [a] ··········· 86ms → [{"id":1},{"id":2}]
|
|
16
39
|
```
|
|
17
40
|
|
|
18
|
-
|
|
19
|
-
[a] ... Fetch users
|
|
20
|
-
[a] ✓ Fetch users 86ms → [{"id":1},{"id":2}]
|
|
21
|
-
[b] ✓ Parse config 0.09ms → {"env":"prod","port":3000}
|
|
22
|
-
```
|
|
41
|
+
## Installation
|
|
23
42
|
|
|
24
|
-
|
|
43
|
+
```sh
|
|
44
|
+
npm install measure-fn
|
|
45
|
+
# or bun add / pnpm add / yarn add
|
|
46
|
+
```
|
|
25
47
|
|
|
26
|
-
|
|
27
|
-
- **catches errors** → logs `✗` with stack trace, returns `null` (no unhandled rejections)
|
|
28
|
-
- **logs timing** → `✓ label Nms → result`
|
|
29
|
-
- **assigns an ID** → `[a]`, `[b]`, `[a-a]` for nested calls
|
|
48
|
+
## ✨ Defaults
|
|
30
49
|
|
|
31
|
-
|
|
50
|
+
Every `measure` call automatically:
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
- 🛡️ **Catches errors** → logs `✗` with a stack trace and returns `null` (no unhandled rejections)
|
|
53
|
+
- ⏱️ **Logs timing** → prints `label Nms → result` using `performance.now()`
|
|
54
|
+
- 🌳 **Assigns a trace ID** → `[a]`, `[b]`, `[a-a]` for zero-config nested hierarchy
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
const user = await measure('Fetch user', () => fetchUser(1));
|
|
37
|
-
// throws → logs ✗, user = null
|
|
38
|
-
```
|
|
56
|
+
## 🌳 Nested Calls (Tracing)
|
|
39
57
|
|
|
40
|
-
Pass `
|
|
58
|
+
Pass a child `m` function to get hierarchical APM-like tracing for free:
|
|
41
59
|
|
|
42
60
|
```typescript
|
|
43
|
-
|
|
44
|
-
(
|
|
45
|
-
);
|
|
46
|
-
|
|
61
|
+
await measure('Pipeline', async (m) => {
|
|
62
|
+
const user = await m('Fetch user', () => fetchUser(1));
|
|
63
|
+
const posts = await m('Fetch posts', () => fetchPosts(user.id));
|
|
64
|
+
return posts;
|
|
65
|
+
});
|
|
47
66
|
```
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
```
|
|
69
|
+
[a] ... Pipeline
|
|
70
|
+
[a-a] ·········· 82ms → {"id":1}
|
|
71
|
+
[a-b] ··········· 45ms → [...]
|
|
72
|
+
[a] ········ 128ms
|
|
73
|
+
```
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
Parallel execution works cleanly too:
|
|
52
76
|
|
|
53
77
|
```typescript
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
await measure('Load all', async (m) => {
|
|
79
|
+
const [users, posts] = await Promise.all([
|
|
80
|
+
m('Users', () => fetchUsers()),
|
|
81
|
+
m('Posts', () => fetchPosts()),
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
56
84
|
```
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|---------|----------|
|
|
60
|
-
| `measure(label, fn)` | returns `null` |
|
|
61
|
-
| `measure(label, fn, onError)` | returns `onError(error)` |
|
|
62
|
-
| `measure.assert(label, fn)` | throws with `.cause` |
|
|
86
|
+
## 🛡️ Error Handling
|
|
63
87
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Set `timeout` in the label object to abort after N milliseconds:
|
|
88
|
+
By default, errors return `null` so your pipelines can continue safely:
|
|
67
89
|
|
|
68
90
|
```typescript
|
|
69
|
-
const
|
|
70
|
-
//
|
|
91
|
+
const user = await measure('Fetch user', () => fetchUser(1));
|
|
92
|
+
// If it throws → logs ✗, user = null
|
|
71
93
|
```
|
|
72
94
|
|
|
73
|
-
|
|
95
|
+
**Custom Fallbacks:** Pass `onError` as the 3rd argument:
|
|
74
96
|
|
|
75
97
|
```typescript
|
|
76
|
-
const
|
|
77
|
-
(error) =>
|
|
98
|
+
const user = await measure('Fetch user', () => fetchUser(1),
|
|
99
|
+
(error) => defaultUser
|
|
78
100
|
);
|
|
101
|
+
// If it throws → logs ✗, user = defaultUser
|
|
79
102
|
```
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
If the `onError` fallback itself throws, that's also safely caught and returns `null`. measure never crashes.
|
|
82
105
|
|
|
83
|
-
|
|
106
|
+
**Fail-Fast (`.assert`):** Use `.assert()` when you need a guaranteed non-null result:
|
|
84
107
|
|
|
85
108
|
```typescript
|
|
86
|
-
await measure(
|
|
87
|
-
//
|
|
109
|
+
const user = await measure.assert('Get user', () => fetchUser(1));
|
|
110
|
+
// If it throws → logs ✗, re-throws with .cause = original error
|
|
88
111
|
```
|
|
89
112
|
|
|
90
|
-
|
|
113
|
+
| Pattern | On error | Return Type |
|
|
114
|
+
|---------|----------|-------------|
|
|
115
|
+
| `measure(label, fn)` | returns `null` | `T \| null` |
|
|
116
|
+
| `measure(label, fn, onError)` | returns `onError(error)` | `T` |
|
|
117
|
+
| `measure.assert(label, fn)` | throws with `.cause` | `T` |
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
await measure({ label: 'Query', budget: 100, timeout: 5000 }, () => query());
|
|
94
|
-
```
|
|
119
|
+
## 🚦 Timeouts & Budgets
|
|
95
120
|
|
|
96
|
-
|
|
121
|
+
The first argument can be a label string, or an options object:
|
|
97
122
|
|
|
98
|
-
|
|
123
|
+
| Field | Type | Effect |
|
|
124
|
+
|-------|------|--------|
|
|
125
|
+
| `label` | `string` | Display name (required if object) |
|
|
126
|
+
| `timeout` | `number` | Aborts after N ms (returns `null`) |
|
|
127
|
+
| `budget` | `number` | Warns if slower than N ms (doesn't abort) |
|
|
128
|
+
| any other | `any` | Logged inline as context metadata |
|
|
129
|
+
|
|
130
|
+
**Timeout** (enforce):
|
|
99
131
|
|
|
100
132
|
```typescript
|
|
101
|
-
await measure('
|
|
102
|
-
|
|
103
|
-
const posts = await m('Fetch posts', () => fetchPosts(user.id));
|
|
104
|
-
return posts;
|
|
105
|
-
});
|
|
133
|
+
const data = await measure({ label: 'Slow API', timeout: 5000 }, () => fetchSlowApi());
|
|
134
|
+
// > 5s → ✗ Slow API 5.0s (Timeout (5.0s)), returns null
|
|
106
135
|
```
|
|
107
136
|
|
|
108
|
-
|
|
109
|
-
[a] ... Pipeline
|
|
110
|
-
[a-a] ✓ Fetch user 82ms → {"id":1}
|
|
111
|
-
[a-b] ✓ Fetch posts 45ms → [...]
|
|
112
|
-
[a] ✓ Pipeline 128ms
|
|
113
|
-
```
|
|
137
|
+
Works with `onError` fallback too.
|
|
114
138
|
|
|
115
|
-
|
|
139
|
+
**Budget** (warn):
|
|
116
140
|
|
|
117
141
|
```typescript
|
|
118
|
-
await measure('
|
|
119
|
-
|
|
120
|
-
m('Users', () => fetchUsers()),
|
|
121
|
-
m('Posts', () => fetchPosts()),
|
|
122
|
-
]);
|
|
123
|
-
});
|
|
142
|
+
await measure({ label: 'DB query', budget: 100 }, () => db.query('...'));
|
|
143
|
+
// → [a] ········ 245ms → [...] ⚠ OVER BUDGET (100ms)
|
|
124
144
|
```
|
|
125
145
|
|
|
126
|
-
|
|
146
|
+
Combine both — budget warns early, timeout enforces a hard stop:
|
|
127
147
|
|
|
128
|
-
|
|
148
|
+
```typescript
|
|
149
|
+
await measure({ label: 'Query', budget: 100, timeout: 5000 }, () => query());
|
|
150
|
+
```
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|-------|------|--------|
|
|
132
|
-
| `label` | `string` | Display name (required if object) |
|
|
133
|
-
| `timeout` | `number` | Abort after N ms |
|
|
134
|
-
| `budget` | `number` | Warn if slower than N ms |
|
|
135
|
-
| any other | `any` | Logged as metadata |
|
|
152
|
+
**Metadata context:**
|
|
136
153
|
|
|
137
154
|
```typescript
|
|
138
|
-
await measure({ label: 'Fetch user', userId: 1
|
|
155
|
+
await measure({ label: 'Fetch user', userId: 1 }, () => fetchUser(1));
|
|
139
156
|
// → [a] ... Fetch user (userId=1)
|
|
140
157
|
```
|
|
141
158
|
|
|
142
|
-
## Extensions
|
|
159
|
+
## 🧰 Extensions
|
|
160
|
+
|
|
161
|
+
### `measure.wrap(label, fn)`
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
Wrap a function once, measure every time it's called:
|
|
145
164
|
|
|
146
165
|
```typescript
|
|
147
166
|
const getUser = measure.wrap('Get user', fetchUser);
|
|
148
|
-
await getUser(1); // → [a]
|
|
149
|
-
await getUser(2); // → [b]
|
|
167
|
+
await getUser(1); // → [a] ········ 82ms
|
|
168
|
+
await getUser(2); // → [b] ········ 75ms
|
|
150
169
|
```
|
|
151
170
|
|
|
152
|
-
### `measure.batch(label, items, fn, opts?)`
|
|
171
|
+
### `measure.batch(label, items, fn, opts?)`
|
|
172
|
+
|
|
173
|
+
Process arrays with built-in progress logs:
|
|
153
174
|
|
|
154
175
|
```typescript
|
|
155
176
|
const results = await measure.batch('Process', userIds, async (id) => {
|
|
@@ -157,26 +178,32 @@ const results = await measure.batch('Process', userIds, async (id) => {
|
|
|
157
178
|
}, { every: 100 });
|
|
158
179
|
// → [a] ... Process (500 items)
|
|
159
180
|
// → [a] = 100/500 (1.2s, 83/s)
|
|
160
|
-
// → [a]
|
|
181
|
+
// → [a] ················· 5.3s → "500/500 ok"
|
|
161
182
|
```
|
|
162
183
|
|
|
163
|
-
### `measure.retry(label, opts, fn)`
|
|
184
|
+
### `measure.retry(label, opts, fn)`
|
|
185
|
+
|
|
186
|
+
Automatic retries with delay and backoff:
|
|
164
187
|
|
|
165
188
|
```typescript
|
|
166
189
|
const result = await measure.retry('Flaky API', {
|
|
167
190
|
attempts: 3, delay: 1000, backoff: 2
|
|
168
191
|
}, () => fetchFlakyApi());
|
|
169
192
|
// → [a] ✗ Flaky API [1/3] 102ms (timeout)
|
|
170
|
-
// → [b]
|
|
193
|
+
// → [b] ················· 89ms → {"status":"ok"}
|
|
171
194
|
```
|
|
172
195
|
|
|
173
|
-
### `measure.timed(label, fn?)`
|
|
196
|
+
### `measure.timed(label, fn?)`
|
|
197
|
+
|
|
198
|
+
Get duration programmatically alongside the result:
|
|
174
199
|
|
|
175
200
|
```typescript
|
|
176
201
|
const { result, duration } = await measure.timed('Fetch', () => fetchUsers());
|
|
177
202
|
```
|
|
178
203
|
|
|
179
|
-
### `createMeasure(prefix)`
|
|
204
|
+
### `createMeasure(prefix)`
|
|
205
|
+
|
|
206
|
+
Scoped instances with custom prefixes:
|
|
180
207
|
|
|
181
208
|
```typescript
|
|
182
209
|
const api = createMeasure('api');
|
|
@@ -186,18 +213,22 @@ await api.measure('GET /users', async () => {
|
|
|
186
213
|
return await db.measure('SELECT', () => query('...'));
|
|
187
214
|
});
|
|
188
215
|
// → [api:a] ... GET /users
|
|
189
|
-
// → [db:a]
|
|
190
|
-
// → [api:a]
|
|
216
|
+
// → [db:a] ······ 44ms
|
|
217
|
+
// → [api:a] ·········· 45ms
|
|
191
218
|
```
|
|
192
219
|
|
|
193
|
-
### Annotations
|
|
220
|
+
### Annotations & Sync
|
|
194
221
|
|
|
195
222
|
```typescript
|
|
223
|
+
import { measureSync } from 'measure-fn';
|
|
224
|
+
|
|
225
|
+
const config = measureSync('Parse config', () => JSON.parse(raw));
|
|
226
|
+
|
|
196
227
|
await measure('Server ready');
|
|
197
228
|
// → [a] = Server ready
|
|
198
229
|
```
|
|
199
230
|
|
|
200
|
-
##
|
|
231
|
+
## ⚙️ Configuration
|
|
201
232
|
|
|
202
233
|
```typescript
|
|
203
234
|
import { configure } from 'measure-fn';
|
|
@@ -206,6 +237,8 @@ configure({
|
|
|
206
237
|
silent: true, // suppress all output
|
|
207
238
|
timestamps: true, // prepend [HH:MM:SS.mmm]
|
|
208
239
|
maxResultLength: 200, // truncate results (default: 80)
|
|
240
|
+
dotEndLabel: false, // show full label on end lines (default: true = dots)
|
|
241
|
+
dotChar: '.', // character for dot fill (default: '·')
|
|
209
242
|
logger: (event) => { // custom event handler
|
|
210
243
|
myTelemetry.track(event);
|
|
211
244
|
}
|
|
@@ -216,12 +249,12 @@ Env vars: `MEASURE_SILENT=1`, `MEASURE_TIMESTAMPS=1`
|
|
|
216
249
|
|
|
217
250
|
## Output Format
|
|
218
251
|
|
|
219
|
-
| Symbol | Meaning |
|
|
220
|
-
|
|
221
|
-
| `[a]
|
|
222
|
-
| `[a]
|
|
223
|
-
| `[a]
|
|
224
|
-
| `[a]
|
|
252
|
+
| Symbol | Meaning | Example |
|
|
253
|
+
|--------|---------|---------|
|
|
254
|
+
| `...` | Started | `[a] ... Fetch users` |
|
|
255
|
+
| `···` | Success | `[a] ··········· 86ms → [...]` |
|
|
256
|
+
| `✗` | Error | `[a] ✗ ··········· (Network Error)` |
|
|
257
|
+
| `=` | Annotation | `[a] = Server ready` |
|
|
225
258
|
|
|
226
259
|
IDs encode hierarchy: `[a]` → root, `[a-a]` → first child, `[a-b]` → second child.
|
|
227
260
|
|
package/index.test.ts
CHANGED
|
@@ -31,12 +31,12 @@ describe("measure (async)", () => {
|
|
|
31
31
|
expect(result).toBe(42);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test("logs start ... and success
|
|
34
|
+
test("logs start ... and success with result", async () => {
|
|
35
35
|
const out = captureConsole();
|
|
36
36
|
await measure("fetch", async () => "ok");
|
|
37
37
|
out.restore();
|
|
38
38
|
expect(out.logs[0]).toBe("[a] ... fetch");
|
|
39
|
-
expect(out.logs[1]).toMatch(/\[a\]
|
|
39
|
+
expect(out.logs[1]).toMatch(/\[a\] ····· .* → "ok"/);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test("no arrow for undefined", async () => {
|
|
@@ -123,7 +123,7 @@ describe("measureSync", () => {
|
|
|
123
123
|
measureSync("compute", () => 100);
|
|
124
124
|
out.restore();
|
|
125
125
|
expect(out.logs.length).toBe(1);
|
|
126
|
-
expect(out.logs[0]).toMatch(/\[a\]
|
|
126
|
+
expect(out.logs[0]).toMatch(/\[a\] ······· .* → 100/);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
test("with children = start + end", () => {
|
|
@@ -131,8 +131,8 @@ describe("measureSync", () => {
|
|
|
131
131
|
measureSync("parent", (m) => { m("child", () => 10); return 20; });
|
|
132
132
|
out.restore();
|
|
133
133
|
expect(out.logs[0]).toBe("[a] ... parent");
|
|
134
|
-
expect(out.logs[1]).toMatch(/\[a-a\]
|
|
135
|
-
expect(out.logs[2]).toMatch(/\[a\]
|
|
134
|
+
expect(out.logs[1]).toMatch(/\[a-a\] ····· .* → 10/);
|
|
135
|
+
expect(out.logs[2]).toMatch(/\[a\] ······ .* → 20/);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
test("error returns null", () => {
|
|
@@ -354,7 +354,7 @@ describe("measure.retry", () => {
|
|
|
354
354
|
const r = await measure.retry("op", { attempts: 3, delay: 10 }, async () => 42);
|
|
355
355
|
out.restore();
|
|
356
356
|
expect(r).toBe(42);
|
|
357
|
-
expect(out.logs[1]).toContain("
|
|
357
|
+
expect(out.logs[1]).not.toContain("✗");
|
|
358
358
|
expect(out.logs[0]).toContain("[1/3]");
|
|
359
359
|
});
|
|
360
360
|
|
|
@@ -368,7 +368,7 @@ describe("measure.retry", () => {
|
|
|
368
368
|
out.restore();
|
|
369
369
|
expect(r).toBe("ok");
|
|
370
370
|
expect(out.logs.filter(l => l.includes("✗")).length).toBe(2);
|
|
371
|
-
expect(out.logs.filter(l => l.includes("
|
|
371
|
+
expect(out.logs.filter(l => !l.includes("✗") && !l.includes("...")).length).toBe(1);
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
test("all attempts exhausted returns null", async () => {
|
|
@@ -589,7 +589,7 @@ describe("measure.wrap", () => {
|
|
|
589
589
|
out.restore();
|
|
590
590
|
expect(r).toBe(42);
|
|
591
591
|
expect(out.logs[0]).toBe("[a] ... double");
|
|
592
|
-
expect(out.logs[1]).toContain("
|
|
592
|
+
expect(out.logs[1]).not.toContain("✗");
|
|
593
593
|
});
|
|
594
594
|
|
|
595
595
|
test("multiple calls get sequential IDs", async () => {
|
|
@@ -610,7 +610,7 @@ describe("measure.wrap", () => {
|
|
|
610
610
|
const r = wrapped(7);
|
|
611
611
|
out.restore();
|
|
612
612
|
expect(r).toBe(21);
|
|
613
|
-
expect(out.logs[0]).toContain("
|
|
613
|
+
expect(out.logs[0]).toContain("······");
|
|
614
614
|
});
|
|
615
615
|
});
|
|
616
616
|
|
|
@@ -628,7 +628,6 @@ describe("measure.batch", () => {
|
|
|
628
628
|
out.restore();
|
|
629
629
|
expect(results).toEqual([2, 4, 6]);
|
|
630
630
|
expect(out.logs[0]).toContain("3 items");
|
|
631
|
-
expect(out.logs.at(-1)).toContain("✓");
|
|
632
631
|
expect(out.logs.at(-1)).toContain("3/3 ok");
|
|
633
632
|
});
|
|
634
633
|
|
package/index.ts
CHANGED
|
@@ -54,7 +54,7 @@ const formatDuration = (ms: number): string => {
|
|
|
54
54
|
// ─── Timestamps ──────────────────────────────────────────────────────
|
|
55
55
|
|
|
56
56
|
let timestamps =
|
|
57
|
-
process.env.MEASURE_TIMESTAMPS === '1' || process.env.MEASURE_TIMESTAMPS === 'true';
|
|
57
|
+
typeof process !== 'undefined' && (process.env.MEASURE_TIMESTAMPS === '1' || process.env.MEASURE_TIMESTAMPS === 'true');
|
|
58
58
|
|
|
59
59
|
const ts = (): string => {
|
|
60
60
|
if (!timestamps) return '';
|
|
@@ -83,7 +83,10 @@ export type MeasureEvent = {
|
|
|
83
83
|
// ─── Configuration ───────────────────────────────────────────────────
|
|
84
84
|
|
|
85
85
|
export let silent =
|
|
86
|
-
process.env.MEASURE_SILENT === '1' || process.env.MEASURE_SILENT === 'true';
|
|
86
|
+
typeof process !== 'undefined' && (process.env.MEASURE_SILENT === '1' || process.env.MEASURE_SILENT === 'true');
|
|
87
|
+
|
|
88
|
+
let dotEndLabel = true;
|
|
89
|
+
let dotChar = '·';
|
|
87
90
|
|
|
88
91
|
export let logger: ((event: MeasureEvent) => void) | null = null;
|
|
89
92
|
|
|
@@ -92,6 +95,8 @@ export type ConfigureOpts = {
|
|
|
92
95
|
logger?: ((event: MeasureEvent) => void) | null;
|
|
93
96
|
timestamps?: boolean;
|
|
94
97
|
maxResultLength?: number;
|
|
98
|
+
dotEndLabel?: boolean;
|
|
99
|
+
dotChar?: string;
|
|
95
100
|
};
|
|
96
101
|
|
|
97
102
|
export const configure = (opts: ConfigureOpts) => {
|
|
@@ -99,6 +104,8 @@ export const configure = (opts: ConfigureOpts) => {
|
|
|
99
104
|
if (opts.logger !== undefined) logger = opts.logger;
|
|
100
105
|
if (opts.timestamps !== undefined) timestamps = opts.timestamps;
|
|
101
106
|
if (opts.maxResultLength !== undefined) maxResultLen = opts.maxResultLength;
|
|
107
|
+
if (opts.dotEndLabel !== undefined) dotEndLabel = opts.dotEndLabel;
|
|
108
|
+
if (opts.dotChar !== undefined) dotChar = opts.dotChar;
|
|
102
109
|
};
|
|
103
110
|
|
|
104
111
|
// ─── Shared Helpers ──────────────────────────────────────────────────
|
|
@@ -157,20 +164,22 @@ const defaultLogger = (event: MeasureEvent, prefix?: string) => {
|
|
|
157
164
|
console.log(`${t}${id} ... ${event.label}${formatMeta(event.meta)}`);
|
|
158
165
|
break;
|
|
159
166
|
case 'success': {
|
|
167
|
+
const endLabel = dotEndLabel ? dotChar.repeat(event.label.length) : event.label;
|
|
160
168
|
const resultStr = event.result !== undefined ? safeStringify(event.result) : '';
|
|
161
169
|
const arrow = resultStr ? ` → ${resultStr}` : '';
|
|
162
170
|
const budgetWarn = event.budget && event.duration! > event.budget
|
|
163
171
|
? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
|
|
164
172
|
: '';
|
|
165
|
-
console.log(`${t}${id}
|
|
173
|
+
console.log(`${t}${id} ${endLabel} ${formatDuration(event.duration!)}${arrow}${budgetWarn}`);
|
|
166
174
|
break;
|
|
167
175
|
}
|
|
168
176
|
case 'error': {
|
|
177
|
+
const endLabel = dotEndLabel ? dotChar.repeat(event.label.length) : event.label;
|
|
169
178
|
const errorMsg = event.error instanceof Error ? event.error.message : String(event.error);
|
|
170
179
|
const budgetWarn = event.budget && event.duration! > event.budget
|
|
171
180
|
? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
|
|
172
181
|
: '';
|
|
173
|
-
console.log(`${t}${id} ✗ ${
|
|
182
|
+
console.log(`${t}${id} ✗ ${endLabel} ${formatDuration(event.duration!)} (${errorMsg})${budgetWarn}`);
|
|
174
183
|
if (event.error instanceof Error) {
|
|
175
184
|
console.error(`${id}`, event.error.stack ?? event.error.message);
|
|
176
185
|
if (event.error.cause) {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"main": "./index.ts",
|
|
5
5
|
"types": "./index.ts",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.7.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"private": false,
|
|
9
9
|
"description": "Zero-dependency function performance measurement with hierarchical logging",
|
|
@@ -30,4 +30,4 @@
|
|
|
30
30
|
"test": "bun test",
|
|
31
31
|
"example": "bun run example.ts"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|