@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,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Module
|
|
3
|
+
*
|
|
4
|
+
* Provides structured error tracking and observability integration for Bueno.
|
|
5
|
+
* Implement ErrorReporter to send events to Sentry, Bugsnag, Datadog, or any
|
|
6
|
+
* custom backend — zero SDK dependencies shipped by the framework.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Start
|
|
9
|
+
*
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { createApp } from '@buenojs/bueno';
|
|
12
|
+
* import { ObservabilityModule } from '@buenojs/bueno/observability';
|
|
13
|
+
*
|
|
14
|
+
* const app = createApp(AppModule);
|
|
15
|
+
*
|
|
16
|
+
* // Wire everything with a single call
|
|
17
|
+
* const obs = ObservabilityModule.setup(app, {
|
|
18
|
+
* reporter: new MyReporter(),
|
|
19
|
+
* breadcrumbsSize: 20,
|
|
20
|
+
* ignoreStatusCodes: [404, 401],
|
|
21
|
+
* tags: { environment: 'production' },
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* await app.listen(3000);
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* ## Writing a reporter (Sentry example)
|
|
28
|
+
*
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import * as Sentry from '@sentry/node';
|
|
31
|
+
* import type { ErrorReporter, ErrorEvent } from '@buenojs/bueno/observability';
|
|
32
|
+
*
|
|
33
|
+
* class SentryReporter implements ErrorReporter {
|
|
34
|
+
* constructor(dsn: string) { Sentry.init({ dsn }); }
|
|
35
|
+
*
|
|
36
|
+
* captureError(event: ErrorEvent) {
|
|
37
|
+
* Sentry.withScope(scope => {
|
|
38
|
+
* if (event.user) scope.setUser(event.user);
|
|
39
|
+
* if (event.traceId) scope.setTag('traceId', event.traceId);
|
|
40
|
+
* event.breadcrumbs.forEach(b => scope.addBreadcrumb(b));
|
|
41
|
+
* Sentry.captureException(event.error);
|
|
42
|
+
* });
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* async flush() { await Sentry.flush(2000); }
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* ObservabilityModule.setup(app, {
|
|
49
|
+
* reporter: new SentryReporter(process.env.SENTRY_DSN),
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import type { Application } from "../modules";
|
|
55
|
+
import { ObservabilityService } from "./service";
|
|
56
|
+
import { ObservabilityInterceptor } from "./interceptor";
|
|
57
|
+
import type { ObservabilityOptions } from "./types";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Static module class for configuring observability.
|
|
61
|
+
* Use `ObservabilityModule.setup(app, options)` for the simplest setup.
|
|
62
|
+
*/
|
|
63
|
+
export class ObservabilityModule {
|
|
64
|
+
/**
|
|
65
|
+
* One-call setup: creates the ObservabilityService, registers the global
|
|
66
|
+
* interceptor for trace ID injection and breadcrumb tracking, and wires
|
|
67
|
+
* process-level shutdown flush.
|
|
68
|
+
*
|
|
69
|
+
* Call this before `app.listen()`.
|
|
70
|
+
*
|
|
71
|
+
* @param app - The Bueno Application instance (from `createApp()`)
|
|
72
|
+
* @param options - Observability configuration including the reporter
|
|
73
|
+
* @returns The ObservabilityService — use for manual captureError / addBreadcrumb
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const obs = ObservabilityModule.setup(app, {
|
|
78
|
+
* reporter: new SentryReporter(process.env.SENTRY_DSN),
|
|
79
|
+
* ignoreStatusCodes: [404, 401],
|
|
80
|
+
* tags: { environment: 'production', version: '1.2.3' },
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* // Later, in a background job:
|
|
84
|
+
* obs.captureError(new Error('Job failed'));
|
|
85
|
+
*
|
|
86
|
+
* // Add a manual breadcrumb:
|
|
87
|
+
* obs.addBreadcrumb({ type: 'custom', level: 'info', message: 'Checkout started' });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
static setup(
|
|
91
|
+
app: Application,
|
|
92
|
+
options: ObservabilityOptions,
|
|
93
|
+
): ObservabilityService {
|
|
94
|
+
const service = new ObservabilityService(options);
|
|
95
|
+
const interceptor = new ObservabilityInterceptor(service);
|
|
96
|
+
|
|
97
|
+
// Register global interceptor for trace ID injection + breadcrumbs + error capture
|
|
98
|
+
app.useGlobalInterceptors(interceptor);
|
|
99
|
+
|
|
100
|
+
// Register flush on process exit
|
|
101
|
+
service.registerShutdown();
|
|
102
|
+
|
|
103
|
+
return service;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============= Public Exports =============
|
|
108
|
+
|
|
109
|
+
// Types
|
|
110
|
+
export type {
|
|
111
|
+
BreadcrumbEntry,
|
|
112
|
+
ErrorEvent,
|
|
113
|
+
ErrorRequestContext,
|
|
114
|
+
ErrorUserContext,
|
|
115
|
+
MessageEvent,
|
|
116
|
+
ErrorReporter,
|
|
117
|
+
ObservabilityOptions,
|
|
118
|
+
ObservabilityConfig,
|
|
119
|
+
} from "./types";
|
|
120
|
+
|
|
121
|
+
// Service & helpers
|
|
122
|
+
export { ObservabilityService } from "./service";
|
|
123
|
+
export { extractTraceContext } from "./service";
|
|
124
|
+
|
|
125
|
+
// Interceptor
|
|
126
|
+
export { ObservabilityInterceptor } from "./interceptor";
|
|
127
|
+
|
|
128
|
+
// Breadcrumb utilities
|
|
129
|
+
export {
|
|
130
|
+
BreadcrumbCollector,
|
|
131
|
+
httpBreadcrumb,
|
|
132
|
+
logBreadcrumb,
|
|
133
|
+
} from "./breadcrumbs";
|
|
134
|
+
|
|
135
|
+
// Trace helpers
|
|
136
|
+
export { generateTraceId, generateSpanId, buildTraceparent } from "./trace";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObservabilityInterceptor
|
|
3
|
+
*
|
|
4
|
+
* Global interceptor that enriches every request with:
|
|
5
|
+
* 1. Trace/span IDs — extracted from W3C `traceparent` header or generated fresh
|
|
6
|
+
* 2. Breadcrumbs — records request entry and exit (with status + duration)
|
|
7
|
+
* 3. Error capture — calls ObservabilityService when a route handler throws
|
|
8
|
+
*
|
|
9
|
+
* Register via ObservabilityModule.setup() (done automatically).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Context } from "../context";
|
|
13
|
+
import type { CallHandler, NestInterceptor } from "../modules";
|
|
14
|
+
import { generateSpanId, generateTraceId } from "./trace";
|
|
15
|
+
import { httpBreadcrumb } from "./breadcrumbs";
|
|
16
|
+
import type { ObservabilityService } from "./service";
|
|
17
|
+
import { extractTraceContext } from "./service";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves trace context from the incoming request.
|
|
21
|
+
* Uses the W3C `traceparent` header if present, otherwise generates new IDs.
|
|
22
|
+
*/
|
|
23
|
+
function resolveTraceIds(context: Context): {
|
|
24
|
+
traceId: string;
|
|
25
|
+
spanId: string;
|
|
26
|
+
} {
|
|
27
|
+
const existing = extractTraceContext(context);
|
|
28
|
+
if (existing.traceId && existing.spanId) {
|
|
29
|
+
return { traceId: existing.traceId, spanId: existing.spanId };
|
|
30
|
+
}
|
|
31
|
+
return { traceId: generateTraceId(), spanId: generateSpanId() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ObservabilityInterceptor implements NestInterceptor {
|
|
35
|
+
constructor(private readonly service: ObservabilityService) {}
|
|
36
|
+
|
|
37
|
+
async intercept(context: Context, next: CallHandler): Promise<unknown> {
|
|
38
|
+
const startMs = Date.now();
|
|
39
|
+
|
|
40
|
+
// 1. Inject trace IDs into context for downstream use
|
|
41
|
+
const { traceId, spanId } = resolveTraceIds(context);
|
|
42
|
+
context.set("traceId", traceId);
|
|
43
|
+
context.set("spanId", spanId);
|
|
44
|
+
|
|
45
|
+
// 2. Record request entry breadcrumb
|
|
46
|
+
this.service.addBreadcrumb({
|
|
47
|
+
type: "navigation",
|
|
48
|
+
level: "info",
|
|
49
|
+
message: `${context.method} ${context.path}`,
|
|
50
|
+
data: { traceId, spanId },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await next.handle();
|
|
55
|
+
|
|
56
|
+
// 3a. Record successful request exit
|
|
57
|
+
const durationMs = Date.now() - startMs;
|
|
58
|
+
const response = result as Response | null;
|
|
59
|
+
const statusCode =
|
|
60
|
+
response instanceof Response ? response.status : undefined;
|
|
61
|
+
|
|
62
|
+
this.service
|
|
63
|
+
.getBreadcrumbCollector()
|
|
64
|
+
.add(
|
|
65
|
+
httpBreadcrumb(context.method, context.path, statusCode, durationMs),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// 3b. Capture the error with full request context (non-blocking)
|
|
71
|
+
this.service.captureFromContext(context, error as Error);
|
|
72
|
+
|
|
73
|
+
// 4. Record failed request breadcrumb
|
|
74
|
+
const durationMs = Date.now() - startMs;
|
|
75
|
+
this.service
|
|
76
|
+
.getBreadcrumbCollector()
|
|
77
|
+
.add(
|
|
78
|
+
httpBreadcrumb(context.method, context.path, undefined, durationMs),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 5. Rethrow so the framework's exception filters still fire
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObservabilityService
|
|
3
|
+
*
|
|
4
|
+
* Central service that assembles ErrorEvents and dispatches them to the
|
|
5
|
+
* configured ErrorReporter. Error capture happens through the
|
|
6
|
+
* ObservabilityInterceptor which calls captureFromContext() directly.
|
|
7
|
+
*
|
|
8
|
+
* Exposes addBreadcrumb() and captureError() for manual instrumentation
|
|
9
|
+
* throughout the application.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Context } from "../context";
|
|
13
|
+
import { BreadcrumbCollector, httpBreadcrumb } from "./breadcrumbs";
|
|
14
|
+
import type {
|
|
15
|
+
BreadcrumbEntry,
|
|
16
|
+
ErrorEvent,
|
|
17
|
+
ErrorReporter,
|
|
18
|
+
ErrorUserContext,
|
|
19
|
+
MessageEvent,
|
|
20
|
+
ObservabilityOptions,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
// ============= Helpers =============
|
|
24
|
+
|
|
25
|
+
function generateId(): string {
|
|
26
|
+
const bytes = new Uint8Array(16);
|
|
27
|
+
crypto.getRandomValues(bytes);
|
|
28
|
+
return Array.from(bytes)
|
|
29
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
30
|
+
.join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractSafeHeaders(req: Request): Record<string, string> {
|
|
34
|
+
const safe: Record<string, string> = {};
|
|
35
|
+
const sensitiveHeaders = new Set([
|
|
36
|
+
"authorization",
|
|
37
|
+
"cookie",
|
|
38
|
+
"x-api-key",
|
|
39
|
+
"x-auth-token",
|
|
40
|
+
"proxy-authorization",
|
|
41
|
+
]);
|
|
42
|
+
req.headers.forEach((value, key) => {
|
|
43
|
+
if (!sensitiveHeaders.has(key.toLowerCase())) {
|
|
44
|
+
safe[key] = value;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return safe;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getStatusCode(error: Error): number | undefined {
|
|
51
|
+
const e = error as Error & { statusCode?: number; status?: number };
|
|
52
|
+
return e.statusCode ?? e.status;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============= Service =============
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* ObservabilityService manages error reporting and breadcrumb tracking.
|
|
59
|
+
*
|
|
60
|
+
* Use ObservabilityModule.setup(app, options) to wire it to the application,
|
|
61
|
+
* or instantiate directly for contexts where the full module isn't needed.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // Via ObservabilityModule (recommended):
|
|
66
|
+
* const obs = ObservabilityModule.setup(app, {
|
|
67
|
+
* reporter: new SentryReporter(),
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* // Manual breadcrumb:
|
|
71
|
+
* obs.addBreadcrumb({
|
|
72
|
+
* type: 'custom',
|
|
73
|
+
* level: 'info',
|
|
74
|
+
* message: 'User completed checkout',
|
|
75
|
+
* data: { orderId: '123' }
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export class ObservabilityService {
|
|
80
|
+
private readonly reporter: ErrorReporter;
|
|
81
|
+
private readonly breadcrumbs: BreadcrumbCollector;
|
|
82
|
+
private readonly options: {
|
|
83
|
+
breadcrumbsSize: number;
|
|
84
|
+
ignoreErrors: Array<new (...args: unknown[]) => Error>;
|
|
85
|
+
ignoreStatusCodes: number[];
|
|
86
|
+
tags: Record<string, string>;
|
|
87
|
+
captureUnhandled: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
constructor(options: ObservabilityOptions) {
|
|
91
|
+
this.reporter = options.reporter;
|
|
92
|
+
this.options = {
|
|
93
|
+
breadcrumbsSize: options.breadcrumbsSize ?? 20,
|
|
94
|
+
ignoreErrors: options.ignoreErrors ?? [],
|
|
95
|
+
ignoreStatusCodes: options.ignoreStatusCodes ?? [],
|
|
96
|
+
tags: options.tags ?? {},
|
|
97
|
+
captureUnhandled: options.captureUnhandled ?? false,
|
|
98
|
+
};
|
|
99
|
+
this.breadcrumbs = new BreadcrumbCollector(this.options.breadcrumbsSize);
|
|
100
|
+
|
|
101
|
+
if (this.options.captureUnhandled) {
|
|
102
|
+
this.setupUnhandledCapture();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============= Framework Integration =============
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Called by ObservabilityInterceptor when an HTTP route throws.
|
|
110
|
+
* Assembles a full ErrorEvent with request context and dispatches it.
|
|
111
|
+
* Non-blocking — never delays the HTTP response.
|
|
112
|
+
*/
|
|
113
|
+
captureFromContext(context: Context, error: Error): void {
|
|
114
|
+
if (this.shouldIgnore(error)) return;
|
|
115
|
+
|
|
116
|
+
// Add a breadcrumb for the failed request entry
|
|
117
|
+
this.breadcrumbs.add(
|
|
118
|
+
httpBreadcrumb(context.method, context.path, undefined),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const event = this.assembleEvent(error, "error", context);
|
|
122
|
+
this.dispatchAsync(event);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Register flush on process shutdown.
|
|
127
|
+
* Called automatically by ObservabilityModule.setup().
|
|
128
|
+
*/
|
|
129
|
+
registerShutdown(): void {
|
|
130
|
+
const flush = async () => {
|
|
131
|
+
if (this.reporter.flush) {
|
|
132
|
+
try {
|
|
133
|
+
await this.reporter.flush();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error("[ObservabilityService] Reporter flush failed:", err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
process.once("beforeExit", () => {
|
|
141
|
+
flush().catch(console.error);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============= Public API =============
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Manually capture an error outside the HTTP pipeline.
|
|
149
|
+
* Useful inside background jobs, event listeners, scheduled tasks.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* obs.captureError(new Error('Payment failed'), 'error');
|
|
153
|
+
*/
|
|
154
|
+
captureError(error: Error, level: ErrorEvent["level"] = "error"): void {
|
|
155
|
+
if (this.shouldIgnore(error)) return;
|
|
156
|
+
const event = this.assembleEvent(error, level);
|
|
157
|
+
this.dispatchAsync(event);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Manually capture a non-error message.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* obs.captureMessage('Deployment completed', 'info', { version: '2.0.0' });
|
|
165
|
+
*/
|
|
166
|
+
captureMessage(
|
|
167
|
+
message: string,
|
|
168
|
+
level: MessageEvent["level"] = "info",
|
|
169
|
+
extra?: Record<string, unknown>,
|
|
170
|
+
): void {
|
|
171
|
+
if (!this.reporter.captureMessage) return;
|
|
172
|
+
const event: MessageEvent = {
|
|
173
|
+
id: generateId(),
|
|
174
|
+
timestamp: new Date(),
|
|
175
|
+
message,
|
|
176
|
+
level,
|
|
177
|
+
extra,
|
|
178
|
+
};
|
|
179
|
+
this.dispatchMessageAsync(event);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Add a breadcrumb to the ring buffer.
|
|
184
|
+
* Breadcrumbs recorded here are included in the next error event.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* obs.addBreadcrumb({
|
|
188
|
+
* type: 'custom',
|
|
189
|
+
* level: 'info',
|
|
190
|
+
* message: 'Fetched user from database',
|
|
191
|
+
* data: { userId: 42 }
|
|
192
|
+
* });
|
|
193
|
+
*/
|
|
194
|
+
addBreadcrumb(
|
|
195
|
+
entry: Omit<BreadcrumbEntry, "timestamp"> & { timestamp?: Date },
|
|
196
|
+
): void {
|
|
197
|
+
this.breadcrumbs.add({
|
|
198
|
+
...entry,
|
|
199
|
+
timestamp: entry.timestamp ?? new Date(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Access the underlying BreadcrumbCollector.
|
|
205
|
+
* Used internally by ObservabilityInterceptor.
|
|
206
|
+
*/
|
|
207
|
+
getBreadcrumbCollector(): BreadcrumbCollector {
|
|
208
|
+
return this.breadcrumbs;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============= Private =============
|
|
212
|
+
|
|
213
|
+
private assembleEvent(
|
|
214
|
+
error: Error,
|
|
215
|
+
level: ErrorEvent["level"],
|
|
216
|
+
context?: Context,
|
|
217
|
+
): ErrorEvent {
|
|
218
|
+
const user = context?.get<ErrorUserContext>("user");
|
|
219
|
+
const traceId = context?.get<string>("traceId") ?? undefined;
|
|
220
|
+
const spanId = context?.get<string>("spanId") ?? undefined;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
id: generateId(),
|
|
224
|
+
timestamp: new Date(),
|
|
225
|
+
error,
|
|
226
|
+
level,
|
|
227
|
+
request: context
|
|
228
|
+
? {
|
|
229
|
+
method: context.method,
|
|
230
|
+
path: context.path,
|
|
231
|
+
headers: extractSafeHeaders(context.req),
|
|
232
|
+
ip: context.ip ?? "",
|
|
233
|
+
userAgent: context.getHeader("user-agent"),
|
|
234
|
+
}
|
|
235
|
+
: undefined,
|
|
236
|
+
traceId,
|
|
237
|
+
spanId,
|
|
238
|
+
user: user ?? undefined,
|
|
239
|
+
breadcrumbs: this.breadcrumbs.getAll(),
|
|
240
|
+
tags: { ...this.options.tags },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private shouldIgnore(error: Error): boolean {
|
|
245
|
+
for (const ErrorClass of this.options.ignoreErrors) {
|
|
246
|
+
if (error instanceof ErrorClass) return true;
|
|
247
|
+
}
|
|
248
|
+
if (this.options.ignoreStatusCodes.length > 0) {
|
|
249
|
+
const code = getStatusCode(error);
|
|
250
|
+
if (code !== undefined && this.options.ignoreStatusCodes.includes(code)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private dispatchAsync(event: ErrorEvent): void {
|
|
258
|
+
// Fire-and-forget — never blocks the HTTP response
|
|
259
|
+
Promise.resolve(this.reporter.captureError(event)).catch((err) => {
|
|
260
|
+
console.error("[ObservabilityService] Reporter.captureError failed:", err);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private dispatchMessageAsync(event: MessageEvent): void {
|
|
265
|
+
if (!this.reporter.captureMessage) return;
|
|
266
|
+
Promise.resolve(this.reporter.captureMessage(event)).catch((err) => {
|
|
267
|
+
console.error(
|
|
268
|
+
"[ObservabilityService] Reporter.captureMessage failed:",
|
|
269
|
+
err,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private setupUnhandledCapture(): void {
|
|
275
|
+
process.on("unhandledRejection", (reason: unknown) => {
|
|
276
|
+
const error =
|
|
277
|
+
reason instanceof Error
|
|
278
|
+
? reason
|
|
279
|
+
: new Error(String(reason ?? "Unhandled rejection"));
|
|
280
|
+
this.captureError(error, "error");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
process.on("uncaughtException", (error: Error) => {
|
|
284
|
+
this.captureError(error, "fatal");
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ============= Trace Context Extraction =============
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extract traceId and spanId from the W3C traceparent header.
|
|
293
|
+
* Format: 00-<traceId>-<spanId>-<flags>
|
|
294
|
+
*/
|
|
295
|
+
export function extractTraceContext(
|
|
296
|
+
context: Context,
|
|
297
|
+
): { traceId?: string; spanId?: string } {
|
|
298
|
+
const traceparent = context.getHeader("traceparent");
|
|
299
|
+
if (!traceparent) return {};
|
|
300
|
+
const parts = traceparent.split("-");
|
|
301
|
+
if (parts.length < 4) return {};
|
|
302
|
+
return { traceId: parts[1], spanId: parts[2] };
|
|
303
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace ID generation helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates W3C TraceContext-compatible IDs using the Web Crypto API.
|
|
5
|
+
* These are intentionally kept separate from src/telemetry to avoid
|
|
6
|
+
* coupling the observability module to the OTLP tracing module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate a W3C-compatible trace ID (32 hex characters / 16 bytes)
|
|
11
|
+
*/
|
|
12
|
+
export function generateTraceId(): string {
|
|
13
|
+
const bytes = new Uint8Array(16);
|
|
14
|
+
crypto.getRandomValues(bytes);
|
|
15
|
+
return Array.from(bytes)
|
|
16
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
17
|
+
.join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a W3C-compatible span ID (16 hex characters / 8 bytes)
|
|
22
|
+
*/
|
|
23
|
+
export function generateSpanId(): string {
|
|
24
|
+
const bytes = new Uint8Array(8);
|
|
25
|
+
crypto.getRandomValues(bytes);
|
|
26
|
+
return Array.from(bytes)
|
|
27
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
28
|
+
.join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a W3C `traceparent` header value from traceId + spanId.
|
|
33
|
+
* Format: 00-<traceId>-<spanId>-01
|
|
34
|
+
*/
|
|
35
|
+
export function buildTraceparent(traceId: string, spanId: string): string {
|
|
36
|
+
return `00-${traceId}-${spanId}-01`;
|
|
37
|
+
}
|