@lovelaces-io/storyteller 0.1.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.cjs +515 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +478 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lovelaces
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Storyteller
|
|
2
|
+
|
|
3
|
+
Lightweight TypeScript logging library that treats logs as **stories** — grouped notes emitted as a single structured event.
|
|
4
|
+
|
|
5
|
+
Zero dependencies. ~24 kB packed. TypeScript-first.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @lovelaces-io/storyteller
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Storyteller } from "@lovelaces-io/storyteller";
|
|
17
|
+
|
|
18
|
+
const story = new Storyteller({
|
|
19
|
+
origin: { where: { app: "checkout", page: "Payment" } },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Collect notes as things happen
|
|
23
|
+
story.note("User submitted payment", {
|
|
24
|
+
who: { id: "user:413" },
|
|
25
|
+
what: { amount: 49.99, currency: "USD" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
story.note("Charging card", {
|
|
29
|
+
what: "stripe:charge",
|
|
30
|
+
where: { service: "payments" },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Tell the story when it's done
|
|
34
|
+
story.tell("Payment completed");
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Notes are bundled into one structured event, delivered to your audiences, and cleared for the next story.
|
|
38
|
+
|
|
39
|
+
## Three Levels
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
story.tell("Payment completed"); // success
|
|
43
|
+
story.warn("Payment slow but succeeded"); // something was off
|
|
44
|
+
story.oops("Payment failed", new Error()); // something broke
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Context on Every Note
|
|
48
|
+
|
|
49
|
+
Every note can carry `who`, `what`, `where`, and `error`:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
story.note("Write failed", {
|
|
53
|
+
who: { id: "user:99" },
|
|
54
|
+
what: { field: "email" },
|
|
55
|
+
where: "primary-db",
|
|
56
|
+
error: new Error("db timeout"),
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Audiences
|
|
61
|
+
|
|
62
|
+
Stories are delivered to **audiences**. Console is included by default. Add your own:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { dbAudience } from "@lovelaces-io/storyteller";
|
|
66
|
+
|
|
67
|
+
// Persist warn and oops events to your database
|
|
68
|
+
story.audience.add(
|
|
69
|
+
dbAudience(async (event) => await db.insert("logs", event))
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Target specific audiences per story
|
|
73
|
+
story.oops("Critical failure", error).to("console", "db");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Summaries
|
|
77
|
+
|
|
78
|
+
Generate a formatted summary without emitting:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const summary = story.summarize({
|
|
82
|
+
title: "Dashboard status",
|
|
83
|
+
level: "tell",
|
|
84
|
+
verbosity: "full",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(summary.text);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Story: Dashboard status
|
|
92
|
+
Level: tell
|
|
93
|
+
Time: Mar 22, 2026, 3:42:18 PM (12ms)
|
|
94
|
+
Origin: checkout / Payment
|
|
95
|
+
Notes:
|
|
96
|
+
3:42:18 PM — User submitted payment
|
|
97
|
+
3:42:18 PM — Charging card
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Shared Instance
|
|
101
|
+
|
|
102
|
+
Use `useStoryteller()` for cross-component logging into the same story:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { useStoryteller } from "@lovelaces-io/storyteller";
|
|
106
|
+
|
|
107
|
+
// Same instance everywhere
|
|
108
|
+
const story = useStoryteller({ origin: { where: { app: "admin" } } });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Structured Output
|
|
112
|
+
|
|
113
|
+
Every story is a typed, serializable JSON object — designed for humans and machines:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"timestamp": "2026-03-22T14:15:03.421Z",
|
|
118
|
+
"level": "oops",
|
|
119
|
+
"title": "Payment failed",
|
|
120
|
+
"origin": { "where": { "app": "checkout", "page": "Payment" } },
|
|
121
|
+
"notes": [
|
|
122
|
+
{
|
|
123
|
+
"timestamp": "2026-03-22T14:15:02.218Z",
|
|
124
|
+
"note": "User submitted payment",
|
|
125
|
+
"who": { "id": "user:413" }
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"error": { "name": "Error", "message": "gateway timeout" }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Docs
|
|
133
|
+
|
|
134
|
+
- [API Reference](docs/API.md) — full signatures and examples
|
|
135
|
+
- [How It Works](docs/HOW-IT-WORKS.md) — narrative guide with real-world scenarios
|
|
136
|
+
- [Changelog](CHANGELOG.md)
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ANSI: () => ANSI,
|
|
24
|
+
Storyteller: () => Storyteller,
|
|
25
|
+
colorizeJsonSections: () => colorizeJsonSections,
|
|
26
|
+
consoleAudience: () => consoleAudience,
|
|
27
|
+
countBrackets: () => countBrackets,
|
|
28
|
+
dbAudience: () => dbAudience,
|
|
29
|
+
formatOrigin: () => formatOrigin,
|
|
30
|
+
getLevelColor: () => getLevelColor,
|
|
31
|
+
summarizeStory: () => summarizeStory,
|
|
32
|
+
useStoryteller: () => useStoryteller,
|
|
33
|
+
writeStoryReport: () => writeStoryReport
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/utils.ts
|
|
38
|
+
var ANSI = {
|
|
39
|
+
reset: "\x1B[0m",
|
|
40
|
+
green: "\x1B[32m",
|
|
41
|
+
yellow: "\x1B[33m",
|
|
42
|
+
red: "\x1B[38;2;250;128;114m",
|
|
43
|
+
grayLight: "\x1B[37m",
|
|
44
|
+
grayDark: "\x1B[37m"
|
|
45
|
+
};
|
|
46
|
+
function getLevelColor(level) {
|
|
47
|
+
if (level === "tell") return ANSI.green;
|
|
48
|
+
if (level === "warn") return ANSI.yellow;
|
|
49
|
+
return ANSI.red;
|
|
50
|
+
}
|
|
51
|
+
function formatOrigin(origin) {
|
|
52
|
+
if (!origin?.where) return;
|
|
53
|
+
if (typeof origin.where === "string") return origin.where;
|
|
54
|
+
const whereRecord = origin.where;
|
|
55
|
+
const parts = [whereRecord.app, whereRecord.service, whereRecord.page, whereRecord.component].filter(Boolean).map(String);
|
|
56
|
+
return parts.length ? parts.join(" / ") : void 0;
|
|
57
|
+
}
|
|
58
|
+
function colorizeJsonSections(json, colors) {
|
|
59
|
+
const lines = json.split("\n");
|
|
60
|
+
let insideNotes = false;
|
|
61
|
+
let bracketDepth = 0;
|
|
62
|
+
return lines.map((line) => {
|
|
63
|
+
if (!insideNotes && line.includes('"notes": [')) {
|
|
64
|
+
insideNotes = true;
|
|
65
|
+
bracketDepth = countBrackets(line);
|
|
66
|
+
return `${colors.notes}${line}${colors.reset}`;
|
|
67
|
+
}
|
|
68
|
+
if (insideNotes) {
|
|
69
|
+
const colored = `${colors.notes}${line}${colors.reset}`;
|
|
70
|
+
bracketDepth += countBrackets(line);
|
|
71
|
+
if (bracketDepth <= 0) insideNotes = false;
|
|
72
|
+
return colored;
|
|
73
|
+
}
|
|
74
|
+
return `${colors.base}${line}${colors.reset}`;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function countBrackets(line) {
|
|
78
|
+
const openCount = (line.match(/\[/g) || []).length;
|
|
79
|
+
const closeCount = (line.match(/\]/g) || []).length;
|
|
80
|
+
return openCount - closeCount;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/audiences/consoleAudience.ts
|
|
84
|
+
function consoleAudience() {
|
|
85
|
+
return {
|
|
86
|
+
name: "console",
|
|
87
|
+
hear: (event) => {
|
|
88
|
+
const prefix = "Storyteller";
|
|
89
|
+
const style = event.level === "tell" ? "color:#16a34a;font-weight:600" : event.level === "warn" ? "color:#f59e0b;font-weight:600" : "color:#dc2626;font-weight:600";
|
|
90
|
+
const header = `${prefix}: ${event.title}`;
|
|
91
|
+
console.groupCollapsed(`%c${header}`, style);
|
|
92
|
+
const payload = JSON.stringify(event, null, 2);
|
|
93
|
+
const coloredPayload = event.level === "oops" ? `${ANSI.red}${payload}${ANSI.reset}` : payload;
|
|
94
|
+
if (event.level === "tell") {
|
|
95
|
+
console.log(header, payload);
|
|
96
|
+
} else if (event.level === "warn") {
|
|
97
|
+
console.warn(header, payload);
|
|
98
|
+
} else {
|
|
99
|
+
console.error(header, coloredPayload);
|
|
100
|
+
}
|
|
101
|
+
console.groupEnd();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/storyteller.ts
|
|
107
|
+
var AudienceRegistry = class {
|
|
108
|
+
members = /* @__PURE__ */ new Map();
|
|
109
|
+
/** Register an audience member, replacing any existing member with the same name */
|
|
110
|
+
add(member) {
|
|
111
|
+
this.members.set(member.name, member);
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
/** Remove an audience member by name */
|
|
115
|
+
remove(name) {
|
|
116
|
+
this.members.delete(name);
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
/** Return all registered audience members */
|
|
120
|
+
getAll() {
|
|
121
|
+
return [...this.members.values()];
|
|
122
|
+
}
|
|
123
|
+
/** Return only the audience members matching the given names */
|
|
124
|
+
getOnly(names) {
|
|
125
|
+
return names.map((name) => this.members.get(name)).filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var Storyteller = class {
|
|
129
|
+
audience = new AudienceRegistry();
|
|
130
|
+
origin;
|
|
131
|
+
notes = [];
|
|
132
|
+
constructor(options) {
|
|
133
|
+
this.origin = options?.origin;
|
|
134
|
+
this.audience.add(consoleAudience());
|
|
135
|
+
options?.audiences?.forEach((audience) => this.audience.add(audience));
|
|
136
|
+
}
|
|
137
|
+
/** Add a timestamped note with optional context (who, what, where, error) */
|
|
138
|
+
note(text, data = {}) {
|
|
139
|
+
this.notes.push({
|
|
140
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
141
|
+
note: text,
|
|
142
|
+
...data.who ? { who: data.who } : {},
|
|
143
|
+
...data.what ? { what: data.what } : {},
|
|
144
|
+
...data.where ? { where: data.where } : {},
|
|
145
|
+
...data.error ? { error: normalizeError(data.error) } : {}
|
|
146
|
+
});
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/** Clear all accumulated notes without emitting a story */
|
|
150
|
+
reset() {
|
|
151
|
+
this.notes = [];
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/** Generate a formatted summary of current notes without emitting or clearing them */
|
|
155
|
+
summarize(options = {}) {
|
|
156
|
+
const {
|
|
157
|
+
title = "Story preview",
|
|
158
|
+
level = "tell",
|
|
159
|
+
error,
|
|
160
|
+
...summaryOptions
|
|
161
|
+
} = options;
|
|
162
|
+
const event = {
|
|
163
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
164
|
+
level,
|
|
165
|
+
title,
|
|
166
|
+
...this.origin ? { origin: this.origin } : {},
|
|
167
|
+
notes: [...this.notes],
|
|
168
|
+
...error ? { error: normalizeError(error) } : {}
|
|
169
|
+
};
|
|
170
|
+
return summarizeStory(event, summaryOptions);
|
|
171
|
+
}
|
|
172
|
+
/** Emit a story at the "tell" level (success / informational) */
|
|
173
|
+
tell(title) {
|
|
174
|
+
return this.createDelivery("tell", title);
|
|
175
|
+
}
|
|
176
|
+
/** Emit a story at the "warn" level (something was off) */
|
|
177
|
+
warn(title) {
|
|
178
|
+
return this.createDelivery("warn", title);
|
|
179
|
+
}
|
|
180
|
+
/** Emit a story at the "oops" level (something broke) with an optional error */
|
|
181
|
+
oops(title, error) {
|
|
182
|
+
return this.createDelivery("oops", title, error);
|
|
183
|
+
}
|
|
184
|
+
/** Build a story event and schedule delivery, returning a handle to override the audience list */
|
|
185
|
+
createDelivery(level, title, error) {
|
|
186
|
+
const event = this.buildEvent(level, title, error);
|
|
187
|
+
let delivered = false;
|
|
188
|
+
let defaultCancelled = false;
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
if (delivered || defaultCancelled) return;
|
|
191
|
+
delivered = true;
|
|
192
|
+
void this.deliver(event);
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
to: (...names) => {
|
|
196
|
+
defaultCancelled = true;
|
|
197
|
+
if (delivered) return;
|
|
198
|
+
delivered = true;
|
|
199
|
+
void this.deliver(event, { only: names });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Assemble the story event from current notes and clear notes for the next story */
|
|
204
|
+
buildEvent(level, title, error) {
|
|
205
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
206
|
+
const collectedNotes = [...this.notes];
|
|
207
|
+
this.notes = [];
|
|
208
|
+
const event = {
|
|
209
|
+
timestamp: now,
|
|
210
|
+
level,
|
|
211
|
+
title,
|
|
212
|
+
...this.origin ? { origin: this.origin } : {},
|
|
213
|
+
notes: collectedNotes,
|
|
214
|
+
...error ? { error: normalizeError(error) } : {}
|
|
215
|
+
};
|
|
216
|
+
const eventWithSummary = event;
|
|
217
|
+
Object.defineProperty(eventWithSummary, "summarize", {
|
|
218
|
+
value: (options) => summarizeStory(event, options),
|
|
219
|
+
enumerable: false
|
|
220
|
+
});
|
|
221
|
+
return eventWithSummary;
|
|
222
|
+
}
|
|
223
|
+
/** Deliver a story event to matching audience members */
|
|
224
|
+
async deliver(event, options) {
|
|
225
|
+
const targets = options?.only?.length ? this.audience.getOnly(options.only) : this.audience.getAll();
|
|
226
|
+
await Promise.allSettled(
|
|
227
|
+
targets.filter((member) => member.accepts ? member.accepts(event) : true).map((member) => member.hear(event))
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
function normalizeError(rawError) {
|
|
232
|
+
if (rawError instanceof Error) {
|
|
233
|
+
const normalized = {
|
|
234
|
+
name: rawError.name,
|
|
235
|
+
message: rawError.message
|
|
236
|
+
};
|
|
237
|
+
if (rawError.stack !== void 0) {
|
|
238
|
+
normalized.stack = rawError.stack;
|
|
239
|
+
}
|
|
240
|
+
const cause = rawError.cause;
|
|
241
|
+
if (cause !== void 0) {
|
|
242
|
+
normalized.cause = cause;
|
|
243
|
+
}
|
|
244
|
+
return normalized;
|
|
245
|
+
}
|
|
246
|
+
return { message: String(rawError) };
|
|
247
|
+
}
|
|
248
|
+
function calculateNoteDuration(notes) {
|
|
249
|
+
if (notes.length <= 1) {
|
|
250
|
+
return {
|
|
251
|
+
durationMs: void 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const startTime = Date.parse(notes[0].timestamp);
|
|
255
|
+
const endTime = Date.parse(notes[notes.length - 1].timestamp);
|
|
256
|
+
return {
|
|
257
|
+
durationMs: Number.isFinite(startTime) && Number.isFinite(endTime) ? Math.max(0, endTime - startTime) : void 0
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function summarizeStory(story, options = {}) {
|
|
261
|
+
const {
|
|
262
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
263
|
+
locale = "en-US",
|
|
264
|
+
verbosity = "normal",
|
|
265
|
+
maxNotes = 50,
|
|
266
|
+
showData = true,
|
|
267
|
+
colorize = true
|
|
268
|
+
} = options;
|
|
269
|
+
const dateTimeFormatter = new Intl.DateTimeFormat(locale, {
|
|
270
|
+
timeZone: timezone,
|
|
271
|
+
year: "numeric",
|
|
272
|
+
month: "short",
|
|
273
|
+
day: "2-digit",
|
|
274
|
+
hour: "numeric",
|
|
275
|
+
minute: "2-digit",
|
|
276
|
+
second: "2-digit"
|
|
277
|
+
});
|
|
278
|
+
const timeFormatter = new Intl.DateTimeFormat(locale, {
|
|
279
|
+
timeZone: timezone,
|
|
280
|
+
hour: "numeric",
|
|
281
|
+
minute: "2-digit",
|
|
282
|
+
second: "2-digit"
|
|
283
|
+
});
|
|
284
|
+
const orderedNotes = [...story.notes].sort(
|
|
285
|
+
(noteA, noteB) => Date.parse(noteA.timestamp) - Date.parse(noteB.timestamp)
|
|
286
|
+
);
|
|
287
|
+
const noteTiming = calculateNoteDuration(orderedNotes);
|
|
288
|
+
const originLabel = formatOrigin(story.origin);
|
|
289
|
+
const duration = noteTiming.durationMs != null ? formatDuration(noteTiming.durationMs) : void 0;
|
|
290
|
+
const slicedNotes = orderedNotes.slice(0, maxNotes);
|
|
291
|
+
const summaryNotes = slicedNotes.map((note) => ({
|
|
292
|
+
timestamp: note.timestamp,
|
|
293
|
+
when: timeFormatter.format(new Date(note.timestamp)),
|
|
294
|
+
note: note.note,
|
|
295
|
+
text: formatNoteText(note, verbosity),
|
|
296
|
+
...note.who ? { who: note.who } : {},
|
|
297
|
+
...note.what ? { what: note.what } : {},
|
|
298
|
+
...note.where ? { where: note.where } : {},
|
|
299
|
+
...note.error ? { error: note.error } : {}
|
|
300
|
+
}));
|
|
301
|
+
const data = {
|
|
302
|
+
title: story.title,
|
|
303
|
+
level: story.level,
|
|
304
|
+
when: dateTimeFormatter.format(new Date(story.timestamp)),
|
|
305
|
+
...noteTiming.durationMs != null ? { durationMs: noteTiming.durationMs } : {},
|
|
306
|
+
...duration ? { duration } : {},
|
|
307
|
+
...story.origin ? { origin: story.origin } : {},
|
|
308
|
+
notes: summaryNotes,
|
|
309
|
+
...story.error ? { error: story.error } : {}
|
|
310
|
+
};
|
|
311
|
+
const levelColor = getLevelColor(story.level);
|
|
312
|
+
const label = (text) => colorize ? `${levelColor}${text}${ANSI.reset}` : text;
|
|
313
|
+
const lines = [];
|
|
314
|
+
lines.push(`${label("Story")}: ${story.title}`);
|
|
315
|
+
lines.push(`${label("Level")}: ${story.level}`);
|
|
316
|
+
lines.push(`${label("Time")}: ${data.when}${duration ? ` (${duration})` : ""}`);
|
|
317
|
+
if (originLabel) {
|
|
318
|
+
lines.push(`${label("Origin")}: ${originLabel}`);
|
|
319
|
+
}
|
|
320
|
+
if (story.error) {
|
|
321
|
+
const errorLine = [story.error.name, story.error.message].filter(Boolean).join(": ");
|
|
322
|
+
if (errorLine) lines.push(`${label("Error")}: ${errorLine}`);
|
|
323
|
+
}
|
|
324
|
+
if (verbosity !== "brief" && summaryNotes.length) {
|
|
325
|
+
lines.push(`${label("Notes")}:`);
|
|
326
|
+
for (const note of summaryNotes) {
|
|
327
|
+
lines.push(` ${note.when} \u2014 ${note.text}`);
|
|
328
|
+
}
|
|
329
|
+
if (orderedNotes.length > summaryNotes.length) {
|
|
330
|
+
lines.push(` \u2026 (${orderedNotes.length - summaryNotes.length} more)`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (showData) {
|
|
334
|
+
lines.push(`${label("Data")}:`);
|
|
335
|
+
const json = JSON.stringify(data, null, 2);
|
|
336
|
+
if (colorize) {
|
|
337
|
+
const colored = colorizeJsonSections(json, {
|
|
338
|
+
base: ANSI.grayLight,
|
|
339
|
+
notes: ANSI.grayDark,
|
|
340
|
+
reset: ANSI.reset
|
|
341
|
+
});
|
|
342
|
+
lines.push(...colored);
|
|
343
|
+
} else {
|
|
344
|
+
lines.push(...json.split("\n"));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { text: lines.join("\n"), data };
|
|
348
|
+
}
|
|
349
|
+
function formatDuration(milliseconds) {
|
|
350
|
+
if (milliseconds < 1e3) return `${milliseconds}ms`;
|
|
351
|
+
const seconds = milliseconds / 1e3;
|
|
352
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
353
|
+
const minutes = Math.floor(seconds / 60);
|
|
354
|
+
const remainingSeconds = Math.round(seconds % 60).toString().padStart(2, "0");
|
|
355
|
+
return `${minutes}:${remainingSeconds}m`;
|
|
356
|
+
}
|
|
357
|
+
function formatNoteText(note, verbosity) {
|
|
358
|
+
if (verbosity !== "full") return note.note;
|
|
359
|
+
const details = [];
|
|
360
|
+
const what = note.what;
|
|
361
|
+
const where = note.where;
|
|
362
|
+
if (typeof what === "string") {
|
|
363
|
+
details.push(`what=${what}`);
|
|
364
|
+
} else if (what) {
|
|
365
|
+
if (what.field) details.push(`field=${String(what.field)}`);
|
|
366
|
+
if (what.status) details.push(`status=${String(what.status)}`);
|
|
367
|
+
}
|
|
368
|
+
if (typeof where === "string") {
|
|
369
|
+
details.push(`where=${where}`);
|
|
370
|
+
} else if (where) {
|
|
371
|
+
if (where.component) details.push(`component=${String(where.component)}`);
|
|
372
|
+
}
|
|
373
|
+
if (note.error) {
|
|
374
|
+
const errorLine = [note.error.name, note.error.message].filter(Boolean).join(": ");
|
|
375
|
+
if (errorLine) details.push(`error=${errorLine}`);
|
|
376
|
+
}
|
|
377
|
+
return details.length ? `${note.note} (${details.join(" ")})` : note.note;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/useStoryteller.ts
|
|
381
|
+
var sharedInstance;
|
|
382
|
+
function useStoryteller(options = {}) {
|
|
383
|
+
if (!sharedInstance || options.reset) {
|
|
384
|
+
sharedInstance = new Storyteller({ origin: options.origin });
|
|
385
|
+
return sharedInstance;
|
|
386
|
+
}
|
|
387
|
+
return sharedInstance;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/audiences/dbAudience.ts
|
|
391
|
+
function dbAudience(insert) {
|
|
392
|
+
return {
|
|
393
|
+
name: "db",
|
|
394
|
+
accepts: (event) => event.level === "warn" || event.level === "oops",
|
|
395
|
+
hear: async (event) => {
|
|
396
|
+
await insert(event);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/report/writeStoryReport.ts
|
|
402
|
+
function writeStoryReport(stories, options = {}) {
|
|
403
|
+
const {
|
|
404
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
405
|
+
locale = "en-US",
|
|
406
|
+
verbosity = "normal",
|
|
407
|
+
maxNotesPerStory = 50,
|
|
408
|
+
showData = true,
|
|
409
|
+
colorize = true
|
|
410
|
+
} = options;
|
|
411
|
+
if (!stories.length) {
|
|
412
|
+
return "Storyteller Report\n\n(no stories)\n";
|
|
413
|
+
}
|
|
414
|
+
const sorted = [...stories].sort(
|
|
415
|
+
(storyA, storyB) => Date.parse(storyA.timestamp) - Date.parse(storyB.timestamp)
|
|
416
|
+
);
|
|
417
|
+
const dateFormatter = new Intl.DateTimeFormat(locale, {
|
|
418
|
+
timeZone: timezone,
|
|
419
|
+
year: "numeric",
|
|
420
|
+
month: "short",
|
|
421
|
+
day: "2-digit"
|
|
422
|
+
});
|
|
423
|
+
const firstStory = sorted[0];
|
|
424
|
+
const lastStory = sorted[sorted.length - 1];
|
|
425
|
+
if (!firstStory || !lastStory) {
|
|
426
|
+
return "Storyteller Report\n\n(no stories)\n";
|
|
427
|
+
}
|
|
428
|
+
const lines = [];
|
|
429
|
+
lines.push(`Storyteller Report (${timezone})`);
|
|
430
|
+
lines.push(
|
|
431
|
+
`Range: ${dateFormatter.format(new Date(firstStory.timestamp))} \u2013 ${dateFormatter.format(
|
|
432
|
+
new Date(lastStory.timestamp)
|
|
433
|
+
)}`
|
|
434
|
+
);
|
|
435
|
+
lines.push("");
|
|
436
|
+
const storiesByDay = /* @__PURE__ */ new Map();
|
|
437
|
+
for (const story of sorted) {
|
|
438
|
+
const dayKey = dateFormatter.format(new Date(story.timestamp));
|
|
439
|
+
const dayEvents = storiesByDay.get(dayKey) ?? [];
|
|
440
|
+
dayEvents.push(story);
|
|
441
|
+
storiesByDay.set(dayKey, dayEvents);
|
|
442
|
+
}
|
|
443
|
+
for (const [day, dayStories] of storiesByDay) {
|
|
444
|
+
lines.push(day);
|
|
445
|
+
for (const story of dayStories) {
|
|
446
|
+
const summary = summarizeStory(story, {
|
|
447
|
+
timezone,
|
|
448
|
+
locale,
|
|
449
|
+
verbosity,
|
|
450
|
+
maxNotes: maxNotesPerStory,
|
|
451
|
+
colorize
|
|
452
|
+
});
|
|
453
|
+
const { data } = summary;
|
|
454
|
+
const originLabel = formatOrigin(story.origin);
|
|
455
|
+
const levelColor = getLevelColor(story.level);
|
|
456
|
+
const label = (text) => colorize ? `${levelColor}${text}${ANSI.reset}` : text;
|
|
457
|
+
const duration = data.duration ? ` (${data.duration})` : "";
|
|
458
|
+
lines.push(`${label("Story")}: ${story.title}`);
|
|
459
|
+
lines.push(`${label("Level")}: ${story.level}`);
|
|
460
|
+
lines.push(`${label("Time")}: ${data.when}${duration}`);
|
|
461
|
+
if (originLabel) {
|
|
462
|
+
lines.push(`${label("Origin")}: ${originLabel}`);
|
|
463
|
+
}
|
|
464
|
+
if (data.error) {
|
|
465
|
+
const errorLine = [
|
|
466
|
+
data.error.name,
|
|
467
|
+
data.error.message
|
|
468
|
+
].filter(Boolean).join(": ");
|
|
469
|
+
if (errorLine) lines.push(`${label("Error")}: ${errorLine}`);
|
|
470
|
+
}
|
|
471
|
+
if (verbosity !== "brief" && data.notes.length) {
|
|
472
|
+
lines.push(` ${label("Notes")}:`);
|
|
473
|
+
for (const summaryNote of data.notes) {
|
|
474
|
+
lines.push(` ${summaryNote.when} \u2014 ${summaryNote.text}`);
|
|
475
|
+
}
|
|
476
|
+
if (story.notes.length > data.notes.length) {
|
|
477
|
+
lines.push(
|
|
478
|
+
` \u2026 (${story.notes.length - data.notes.length} more)`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (showData) {
|
|
483
|
+
lines.push(`${label("Data")}:`);
|
|
484
|
+
const json = JSON.stringify(data, null, 2);
|
|
485
|
+
if (colorize) {
|
|
486
|
+
const colored = colorizeJsonSections(json, {
|
|
487
|
+
base: ANSI.grayLight,
|
|
488
|
+
notes: ANSI.grayDark,
|
|
489
|
+
reset: ANSI.reset
|
|
490
|
+
});
|
|
491
|
+
lines.push(...colored);
|
|
492
|
+
} else {
|
|
493
|
+
lines.push(...json.split("\n"));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
lines.push("");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return lines.join("\n").trim() + "\n";
|
|
500
|
+
}
|
|
501
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
502
|
+
0 && (module.exports = {
|
|
503
|
+
ANSI,
|
|
504
|
+
Storyteller,
|
|
505
|
+
colorizeJsonSections,
|
|
506
|
+
consoleAudience,
|
|
507
|
+
countBrackets,
|
|
508
|
+
dbAudience,
|
|
509
|
+
formatOrigin,
|
|
510
|
+
getLevelColor,
|
|
511
|
+
summarizeStory,
|
|
512
|
+
useStoryteller,
|
|
513
|
+
writeStoryReport
|
|
514
|
+
});
|
|
515
|
+
//# sourceMappingURL=index.cjs.map
|