@buenojs/bueno 0.8.4 → 0.8.6
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 +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Module Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for BreadcrumbCollector, ObservabilityService, ObservabilityInterceptor,
|
|
5
|
+
* and trace utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
9
|
+
import { Context } from "../../src/context";
|
|
10
|
+
import {
|
|
11
|
+
BreadcrumbCollector,
|
|
12
|
+
httpBreadcrumb,
|
|
13
|
+
logBreadcrumb,
|
|
14
|
+
} from "../../src/observability/breadcrumbs";
|
|
15
|
+
import { ObservabilityService, extractTraceContext } from "../../src/observability/service";
|
|
16
|
+
import { ObservabilityInterceptor } from "../../src/observability/interceptor";
|
|
17
|
+
import {
|
|
18
|
+
generateTraceId,
|
|
19
|
+
generateSpanId,
|
|
20
|
+
buildTraceparent,
|
|
21
|
+
} from "../../src/observability/trace";
|
|
22
|
+
import type { ErrorEvent, MessageEvent, ErrorReporter } from "../../src/observability/types";
|
|
23
|
+
|
|
24
|
+
// ============= Helpers =============
|
|
25
|
+
|
|
26
|
+
function makeContext(
|
|
27
|
+
path = "/test",
|
|
28
|
+
method = "GET",
|
|
29
|
+
headers: Record<string, string> = {},
|
|
30
|
+
): Context {
|
|
31
|
+
const req = new Request(`http://localhost${path}`, { method, headers });
|
|
32
|
+
return new Context(req);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class CollectingReporter implements ErrorReporter {
|
|
36
|
+
errors: ErrorEvent[] = [];
|
|
37
|
+
messages: MessageEvent[] = [];
|
|
38
|
+
flushed = false;
|
|
39
|
+
|
|
40
|
+
captureError(event: ErrorEvent): void {
|
|
41
|
+
this.errors.push(event);
|
|
42
|
+
}
|
|
43
|
+
captureMessage(event: MessageEvent): void {
|
|
44
|
+
this.messages.push(event);
|
|
45
|
+
}
|
|
46
|
+
async flush(): Promise<void> {
|
|
47
|
+
this.flushed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============= BreadcrumbCollector =============
|
|
52
|
+
|
|
53
|
+
describe("BreadcrumbCollector", () => {
|
|
54
|
+
test("stores breadcrumbs in order", () => {
|
|
55
|
+
const collector = new BreadcrumbCollector(5);
|
|
56
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "a" });
|
|
57
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "b" });
|
|
58
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "c" });
|
|
59
|
+
|
|
60
|
+
const all = collector.getAll();
|
|
61
|
+
expect(all).toHaveLength(3);
|
|
62
|
+
expect(all[0].message).toBe("a");
|
|
63
|
+
expect(all[2].message).toBe("c");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("evicts oldest entry when buffer is full", () => {
|
|
67
|
+
const collector = new BreadcrumbCollector(3);
|
|
68
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "1" });
|
|
69
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "2" });
|
|
70
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "3" });
|
|
71
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "4" });
|
|
72
|
+
|
|
73
|
+
const all = collector.getAll();
|
|
74
|
+
expect(all).toHaveLength(3);
|
|
75
|
+
expect(all[0].message).toBe("2");
|
|
76
|
+
expect(all[2].message).toBe("4");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("handles rapid overflow correctly", () => {
|
|
80
|
+
const collector = new BreadcrumbCollector(2);
|
|
81
|
+
for (let i = 1; i <= 5; i++) {
|
|
82
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: String(i) });
|
|
83
|
+
}
|
|
84
|
+
const all = collector.getAll();
|
|
85
|
+
expect(all).toHaveLength(2);
|
|
86
|
+
expect(all[0].message).toBe("4");
|
|
87
|
+
expect(all[1].message).toBe("5");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("reports correct size", () => {
|
|
91
|
+
const collector = new BreadcrumbCollector(10);
|
|
92
|
+
expect(collector.size).toBe(0);
|
|
93
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "x" });
|
|
94
|
+
expect(collector.size).toBe(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("never exceeds maxSize", () => {
|
|
98
|
+
const collector = new BreadcrumbCollector(3);
|
|
99
|
+
for (let i = 0; i < 10; i++) {
|
|
100
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: String(i) });
|
|
101
|
+
}
|
|
102
|
+
expect(collector.size).toBe(3);
|
|
103
|
+
expect(collector.maxSize).toBe(3);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("clear() empties the buffer", () => {
|
|
107
|
+
const collector = new BreadcrumbCollector(5);
|
|
108
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "a" });
|
|
109
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "b" });
|
|
110
|
+
collector.clear();
|
|
111
|
+
expect(collector.size).toBe(0);
|
|
112
|
+
expect(collector.getAll()).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("throws on invalid maxSize", () => {
|
|
116
|
+
expect(() => new BreadcrumbCollector(0)).toThrow(RangeError);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("getAll() returns empty array when empty", () => {
|
|
120
|
+
expect(new BreadcrumbCollector(5).getAll()).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("single-slot buffer evicts correctly", () => {
|
|
124
|
+
const collector = new BreadcrumbCollector(1);
|
|
125
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "a" });
|
|
126
|
+
collector.add({ timestamp: new Date(), type: "log", level: "info", message: "b" });
|
|
127
|
+
const all = collector.getAll();
|
|
128
|
+
expect(all).toHaveLength(1);
|
|
129
|
+
expect(all[0].message).toBe("b");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ============= Breadcrumb Helpers =============
|
|
134
|
+
|
|
135
|
+
describe("httpBreadcrumb", () => {
|
|
136
|
+
test("creates a basic http breadcrumb", () => {
|
|
137
|
+
const crumb = httpBreadcrumb("GET", "/users");
|
|
138
|
+
expect(crumb.type).toBe("http");
|
|
139
|
+
expect(crumb.message).toBe("GET /users");
|
|
140
|
+
expect(crumb.level).toBe("info");
|
|
141
|
+
expect(crumb.data?.method).toBe("GET");
|
|
142
|
+
expect(crumb.data?.path).toBe("/users");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("includes status code and duration", () => {
|
|
146
|
+
const crumb = httpBreadcrumb("POST", "/orders", 201, 42);
|
|
147
|
+
expect(crumb.message).toBe("POST /orders 201");
|
|
148
|
+
expect(crumb.data?.statusCode).toBe(201);
|
|
149
|
+
expect(crumb.data?.durationMs).toBe(42);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("marks error level for 4xx/5xx responses", () => {
|
|
153
|
+
expect(httpBreadcrumb("GET", "/notfound", 404).level).toBe("error");
|
|
154
|
+
expect(httpBreadcrumb("GET", "/error", 500).level).toBe("error");
|
|
155
|
+
expect(httpBreadcrumb("GET", "/ok", 200).level).toBe("info");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("logBreadcrumb", () => {
|
|
160
|
+
test("creates a log breadcrumb with data", () => {
|
|
161
|
+
const crumb = logBreadcrumb("warning", "Slow query", { ms: 2000 });
|
|
162
|
+
expect(crumb.type).toBe("log");
|
|
163
|
+
expect(crumb.level).toBe("warning");
|
|
164
|
+
expect(crumb.message).toBe("Slow query");
|
|
165
|
+
expect(crumb.data?.ms).toBe(2000);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ============= Trace Utilities =============
|
|
170
|
+
|
|
171
|
+
describe("Trace utilities", () => {
|
|
172
|
+
test("generateTraceId returns 32-char hex string", () => {
|
|
173
|
+
const id = generateTraceId();
|
|
174
|
+
expect(id).toHaveLength(32);
|
|
175
|
+
expect(/^[0-9a-f]+$/.test(id)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("generateSpanId returns 16-char hex string", () => {
|
|
179
|
+
const id = generateSpanId();
|
|
180
|
+
expect(id).toHaveLength(16);
|
|
181
|
+
expect(/^[0-9a-f]+$/.test(id)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("generateTraceId produces unique values", () => {
|
|
185
|
+
const ids = new Set(Array.from({ length: 100 }, generateTraceId));
|
|
186
|
+
expect(ids.size).toBe(100);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("buildTraceparent formats correctly", () => {
|
|
190
|
+
expect(buildTraceparent("aaa", "bbb")).toBe("00-aaa-bbb-01");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("extractTraceContext parses valid traceparent header", () => {
|
|
194
|
+
const traceId = generateTraceId();
|
|
195
|
+
const spanId = generateSpanId();
|
|
196
|
+
const ctx = makeContext("/test", "GET", {
|
|
197
|
+
traceparent: buildTraceparent(traceId, spanId),
|
|
198
|
+
});
|
|
199
|
+
const extracted = extractTraceContext(ctx);
|
|
200
|
+
expect(extracted.traceId).toBe(traceId);
|
|
201
|
+
expect(extracted.spanId).toBe(spanId);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("extractTraceContext returns empty for missing header", () => {
|
|
205
|
+
expect(extractTraceContext(makeContext("/test"))).toEqual({});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("extractTraceContext returns empty for malformed header", () => {
|
|
209
|
+
const ctx = makeContext("/test", "GET", { traceparent: "bad-format" });
|
|
210
|
+
expect(extractTraceContext(ctx)).toEqual({});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ============= ObservabilityService =============
|
|
215
|
+
|
|
216
|
+
describe("ObservabilityService", () => {
|
|
217
|
+
let reporter: CollectingReporter;
|
|
218
|
+
let service: ObservabilityService;
|
|
219
|
+
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
reporter = new CollectingReporter();
|
|
222
|
+
service = new ObservabilityService({ reporter });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("captureError dispatches to reporter asynchronously", async () => {
|
|
226
|
+
const error = new Error("test error");
|
|
227
|
+
service.captureError(error);
|
|
228
|
+
await Promise.resolve();
|
|
229
|
+
expect(reporter.errors).toHaveLength(1);
|
|
230
|
+
expect(reporter.errors[0].error).toBe(error);
|
|
231
|
+
expect(reporter.errors[0].level).toBe("error");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("captureError uses specified level", async () => {
|
|
235
|
+
service.captureError(new Error("fatal"), "fatal");
|
|
236
|
+
await Promise.resolve();
|
|
237
|
+
expect(reporter.errors[0].level).toBe("fatal");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("captureError includes accumulated breadcrumbs", async () => {
|
|
241
|
+
service.addBreadcrumb({ type: "custom", level: "info", message: "step 1" });
|
|
242
|
+
service.addBreadcrumb({ type: "custom", level: "info", message: "step 2" });
|
|
243
|
+
service.captureError(new Error("oops"));
|
|
244
|
+
await Promise.resolve();
|
|
245
|
+
const crumbs = reporter.errors[0].breadcrumbs;
|
|
246
|
+
expect(crumbs.some((c) => c.message === "step 1")).toBe(true);
|
|
247
|
+
expect(crumbs.some((c) => c.message === "step 2")).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("captureFromContext attaches request metadata", async () => {
|
|
251
|
+
const ctx = makeContext("/api/users", "POST");
|
|
252
|
+
service.captureFromContext(ctx, new Error("req error"));
|
|
253
|
+
await Promise.resolve();
|
|
254
|
+
const event = reporter.errors[0];
|
|
255
|
+
expect(event.request?.method).toBe("POST");
|
|
256
|
+
expect(event.request?.path).toBe("/api/users");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("captureFromContext reads traceId / spanId from context", async () => {
|
|
260
|
+
const ctx = makeContext("/test");
|
|
261
|
+
ctx.set("traceId", "abc123trace");
|
|
262
|
+
ctx.set("spanId", "span456");
|
|
263
|
+
service.captureFromContext(ctx, new Error("traced"));
|
|
264
|
+
await Promise.resolve();
|
|
265
|
+
expect(reporter.errors[0].traceId).toBe("abc123trace");
|
|
266
|
+
expect(reporter.errors[0].spanId).toBe("span456");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("captureFromContext reads user from context", async () => {
|
|
270
|
+
const ctx = makeContext("/test");
|
|
271
|
+
ctx.set("user", { id: 42, email: "alice@example.com" });
|
|
272
|
+
service.captureFromContext(ctx, new Error("user error"));
|
|
273
|
+
await Promise.resolve();
|
|
274
|
+
expect(reporter.errors[0].user?.id).toBe(42);
|
|
275
|
+
expect(reporter.errors[0].user?.email).toBe("alice@example.com");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("captureFromContext strips sensitive headers", async () => {
|
|
279
|
+
const ctx = makeContext("/test", "GET", {
|
|
280
|
+
authorization: "Bearer secret",
|
|
281
|
+
"x-api-key": "key123",
|
|
282
|
+
"content-type": "application/json",
|
|
283
|
+
});
|
|
284
|
+
service.captureFromContext(ctx, new Error("header test"));
|
|
285
|
+
await Promise.resolve();
|
|
286
|
+
const headers = reporter.errors[0].request?.headers ?? {};
|
|
287
|
+
expect("authorization" in headers).toBe(false);
|
|
288
|
+
expect("x-api-key" in headers).toBe(false);
|
|
289
|
+
expect(headers["content-type"]).toBe("application/json");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("ignoreErrors suppresses matching error types", async () => {
|
|
293
|
+
class CustomError extends Error {}
|
|
294
|
+
const svc = new ObservabilityService({ reporter, ignoreErrors: [CustomError] });
|
|
295
|
+
svc.captureError(new CustomError("ignored"));
|
|
296
|
+
await Promise.resolve();
|
|
297
|
+
expect(reporter.errors).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("ignoreStatusCodes suppresses matching codes", async () => {
|
|
301
|
+
const svc = new ObservabilityService({ reporter, ignoreStatusCodes: [404] });
|
|
302
|
+
const err = Object.assign(new Error("not found"), { statusCode: 404 });
|
|
303
|
+
svc.captureError(err);
|
|
304
|
+
await Promise.resolve();
|
|
305
|
+
expect(reporter.errors).toHaveLength(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("ignoreStatusCodes allows non-matching codes", async () => {
|
|
309
|
+
const svc = new ObservabilityService({ reporter, ignoreStatusCodes: [404] });
|
|
310
|
+
const err = Object.assign(new Error("server error"), { statusCode: 500 });
|
|
311
|
+
svc.captureError(err);
|
|
312
|
+
await Promise.resolve();
|
|
313
|
+
expect(reporter.errors).toHaveLength(1);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("tags are attached to every event", async () => {
|
|
317
|
+
const svc = new ObservabilityService({
|
|
318
|
+
reporter,
|
|
319
|
+
tags: { environment: "test", version: "1.0.0" },
|
|
320
|
+
});
|
|
321
|
+
svc.captureError(new Error("tagged"));
|
|
322
|
+
await Promise.resolve();
|
|
323
|
+
expect(reporter.errors[0].tags?.environment).toBe("test");
|
|
324
|
+
expect(reporter.errors[0].tags?.version).toBe("1.0.0");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("captureMessage sends to reporter", async () => {
|
|
328
|
+
service.captureMessage("Hello", "info", { key: "val" });
|
|
329
|
+
await Promise.resolve();
|
|
330
|
+
expect(reporter.messages).toHaveLength(1);
|
|
331
|
+
expect(reporter.messages[0].message).toBe("Hello");
|
|
332
|
+
expect(reporter.messages[0].level).toBe("info");
|
|
333
|
+
expect(reporter.messages[0].extra?.key).toBe("val");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("addBreadcrumb defaults timestamp to now", () => {
|
|
337
|
+
const before = new Date();
|
|
338
|
+
service.addBreadcrumb({ type: "custom", level: "debug", message: "x" });
|
|
339
|
+
const after = new Date();
|
|
340
|
+
const crumb = service.getBreadcrumbCollector().getAll()[0];
|
|
341
|
+
expect(crumb.timestamp >= before).toBe(true);
|
|
342
|
+
expect(crumb.timestamp <= after).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("error events have unique IDs", async () => {
|
|
346
|
+
service.captureError(new Error("e1"));
|
|
347
|
+
service.captureError(new Error("e2"));
|
|
348
|
+
await Promise.resolve();
|
|
349
|
+
await Promise.resolve();
|
|
350
|
+
const ids = reporter.errors.map((e) => e.id);
|
|
351
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("breadcrumbsSize option is respected", () => {
|
|
355
|
+
const svc = new ObservabilityService({ reporter, breadcrumbsSize: 3 });
|
|
356
|
+
expect(svc.getBreadcrumbCollector().maxSize).toBe(3);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ============= ObservabilityInterceptor =============
|
|
361
|
+
|
|
362
|
+
describe("ObservabilityInterceptor", () => {
|
|
363
|
+
let reporter: CollectingReporter;
|
|
364
|
+
let service: ObservabilityService;
|
|
365
|
+
let interceptor: ObservabilityInterceptor;
|
|
366
|
+
|
|
367
|
+
beforeEach(() => {
|
|
368
|
+
reporter = new CollectingReporter();
|
|
369
|
+
service = new ObservabilityService({ reporter });
|
|
370
|
+
interceptor = new ObservabilityInterceptor(service);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("injects traceId and spanId into context", async () => {
|
|
374
|
+
const ctx = makeContext("/test");
|
|
375
|
+
await interceptor.intercept(ctx, { handle: async () => new Response("ok") });
|
|
376
|
+
expect(ctx.get("traceId")).toBeTruthy();
|
|
377
|
+
expect(ctx.get("spanId")).toBeTruthy();
|
|
378
|
+
expect(String(ctx.get("traceId"))).toHaveLength(32);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("uses existing traceId from traceparent header", async () => {
|
|
382
|
+
const traceId = generateTraceId();
|
|
383
|
+
const spanId = generateSpanId();
|
|
384
|
+
const ctx = makeContext("/test", "GET", {
|
|
385
|
+
traceparent: buildTraceparent(traceId, spanId),
|
|
386
|
+
});
|
|
387
|
+
await interceptor.intercept(ctx, { handle: async () => new Response("ok") });
|
|
388
|
+
expect(ctx.get("traceId")).toBe(traceId);
|
|
389
|
+
expect(ctx.get("spanId")).toBe(spanId);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("adds navigation breadcrumb on request entry", async () => {
|
|
393
|
+
const ctx = makeContext("/api/items", "GET");
|
|
394
|
+
await interceptor.intercept(ctx, { handle: async () => new Response("ok") });
|
|
395
|
+
const crumbs = service.getBreadcrumbCollector().getAll();
|
|
396
|
+
const nav = crumbs.find((c) => c.type === "navigation");
|
|
397
|
+
expect(nav).toBeDefined();
|
|
398
|
+
expect(nav?.message).toBe("GET /api/items");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("adds http breadcrumb with status code on success", async () => {
|
|
402
|
+
const ctx = makeContext("/api/items", "GET");
|
|
403
|
+
await interceptor.intercept(ctx, {
|
|
404
|
+
handle: async () => new Response("ok", { status: 200 }),
|
|
405
|
+
});
|
|
406
|
+
const crumbs = service.getBreadcrumbCollector().getAll();
|
|
407
|
+
const http = crumbs.find((c) => c.type === "http" && c.data?.statusCode === 200);
|
|
408
|
+
expect(http).toBeDefined();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("captures error and rethrows when handler throws", async () => {
|
|
412
|
+
const ctx = makeContext("/api/fail", "DELETE");
|
|
413
|
+
const boom = new Error("Handler exploded");
|
|
414
|
+
|
|
415
|
+
let threw = false;
|
|
416
|
+
try {
|
|
417
|
+
await interceptor.intercept(ctx, {
|
|
418
|
+
handle: async () => { throw boom; },
|
|
419
|
+
});
|
|
420
|
+
} catch (e) {
|
|
421
|
+
threw = true;
|
|
422
|
+
expect(e).toBe(boom);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
expect(threw).toBe(true);
|
|
426
|
+
await Promise.resolve();
|
|
427
|
+
expect(reporter.errors).toHaveLength(1);
|
|
428
|
+
expect(reporter.errors[0].error).toBe(boom);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("error event includes request context", async () => {
|
|
432
|
+
const ctx = makeContext("/api/protected", "POST");
|
|
433
|
+
ctx.set("user", { id: 7, email: "bob@example.com" });
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await interceptor.intercept(ctx, {
|
|
437
|
+
handle: async () => { throw new Error("auth failed"); },
|
|
438
|
+
});
|
|
439
|
+
} catch { /* expected */ }
|
|
440
|
+
|
|
441
|
+
await Promise.resolve();
|
|
442
|
+
const event = reporter.errors[0];
|
|
443
|
+
expect(event.request?.path).toBe("/api/protected");
|
|
444
|
+
expect(event.user?.id).toBe(7);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("passes through handler result on success", async () => {
|
|
448
|
+
const ctx = makeContext("/test");
|
|
449
|
+
const response = new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
450
|
+
const result = await interceptor.intercept(ctx, { handle: async () => response });
|
|
451
|
+
expect(result).toBe(response);
|
|
452
|
+
});
|
|
453
|
+
});
|