@solenai/monitor 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/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @solen/monitor
2
+
3
+ Report runtime errors to Solen MetaShift incident webhooks. Payloads use a Sentry-compatible shape so MetaShift normalizes them automatically. No runtime dependencies—Node 18+ built-in `fetch` only.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @solen/monitor
9
+ ```
10
+
11
+ ## Quick setup
12
+
13
+ ```ts
14
+ import { monitor } from '@solen/monitor';
15
+
16
+ const { captureError } = monitor({
17
+ webhookUrl: process.env.SOLEN_WEBHOOK_URL!,
18
+ secret: process.env.SOLEN_WEBHOOK_SECRET!,
19
+ environment: 'production',
20
+ });
21
+ ```
22
+
23
+ Calling `monitor()` registers `uncaughtException` and `unhandledRejection` handlers and returns manual capture helpers.
24
+
25
+ ## Express middleware
26
+
27
+ ```ts
28
+ import express from 'express';
29
+ import { monitor } from '@solen/monitor';
30
+ import { solenErrorHandler } from '@solen/monitor/express';
31
+
32
+ const { captureError } = monitor({
33
+ webhookUrl: process.env.SOLEN_WEBHOOK_URL!,
34
+ secret: process.env.SOLEN_WEBHOOK_SECRET!,
35
+ });
36
+
37
+ const app = express();
38
+ app.use(solenErrorHandler({ captureError }));
39
+ ```
40
+
41
+ `solenErrorHandler` captures the error with route, method, and status, then calls `next(err)` so your existing error handlers still run.
42
+
43
+ ## Manual capture
44
+
45
+ ```ts
46
+ import { monitor } from '@solen/monitor';
47
+
48
+ const { captureError, captureMessage } = monitor({
49
+ webhookUrl: process.env.SOLEN_WEBHOOK_URL!,
50
+ secret: process.env.SOLEN_WEBHOOK_SECRET!,
51
+ });
52
+
53
+ try {
54
+ await chargeCustomer(orderId);
55
+ } catch (err) {
56
+ await captureError(err, {
57
+ extra: { orderId },
58
+ tags: { service: 'billing' },
59
+ });
60
+ throw err;
61
+ }
62
+
63
+ await captureMessage('Stripe webhook replay detected', { level: 'warning' });
64
+ ```
65
+
66
+ ## Webhook URL and secret
67
+
68
+ Create an incident webhook endpoint in the Solen dashboard:
69
+
70
+ **https://solenai.ca/integrations/webhooks**
71
+
72
+ Each endpoint provides:
73
+
74
+ - **Webhook URL** — MetaShift ingest URL (`/api/webhooks/incident/{endpointId}`)
75
+ - **Secret** — sent as the `X-Solen-Secret` header on every request
76
+
77
+ Store both in environment variables (for example `SOLEN_WEBHOOK_URL` and `SOLEN_WEBHOOK_SECRET`) and pass them to `monitor()`.
78
+
79
+ ## Fastify
80
+
81
+ ```ts
82
+ import Fastify from 'fastify';
83
+ import { monitor } from '@solen/monitor';
84
+ import { solenFastifyPlugin } from '@solen/monitor/fastify';
85
+
86
+ const { captureError } = monitor({ webhookUrl, secret });
87
+ const app = Fastify();
88
+ await app.register(solenFastifyPlugin, { captureError });
89
+ ```
90
+
91
+ ## Requirements
92
+
93
+ - Node.js 18+
94
+ - Optional peers: `express` (for `@solen/monitor/express`), `fastify` (for `@solen/monitor/fastify`)
@@ -0,0 +1,27 @@
1
+ import type { CaptureContext, CaptureLevel, SentryWebhookPayload, SolenMonitorOptions } from './types.js';
2
+ export declare class SolenMonitorError extends Error {
3
+ readonly status: number;
4
+ readonly body: unknown;
5
+ constructor(message: string, status: number, body: unknown);
6
+ }
7
+ export declare function buildSentryWebhookPayload(params: {
8
+ title: string;
9
+ message?: string;
10
+ level?: CaptureLevel;
11
+ environment?: string;
12
+ url?: string;
13
+ tags?: Record<string, string>;
14
+ extra?: Record<string, unknown>;
15
+ }): SentryWebhookPayload;
16
+ export declare class SolenMonitor {
17
+ private readonly webhookUrl;
18
+ private readonly secret;
19
+ constructor(options: SolenMonitorOptions);
20
+ /** Report an exception using a Sentry-compatible webhook payload. */
21
+ captureException(error: unknown, context?: CaptureContext): Promise<void>;
22
+ /** Report a message using a Sentry-compatible webhook payload. */
23
+ captureMessage(message: string, context?: CaptureContext): Promise<void>;
24
+ /** POST the payload to the configured webhook. */
25
+ send(payload: SentryWebhookPayload): Promise<void>;
26
+ }
27
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB,qBAAa,iBAAkB,SAAQ,KAAK;IAGxC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO;gBAFtB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO;CAKzB;AAgCD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,GAAG,oBAAoB,CAkCvB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,EAAE,mBAAmB;IAKxC,qEAAqE;IAC/D,gBAAgB,CACpB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;IAgBhB,kEAAkE;IAC5D,cAAc,CAClB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC;IAkBhB,kDAAkD;IAC5C,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;CA8BzD"}
package/dist/client.js ADDED
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SolenMonitor = exports.SolenMonitorError = void 0;
4
+ exports.buildSentryWebhookPayload = buildSentryWebhookPayload;
5
+ class SolenMonitorError extends Error {
6
+ status;
7
+ body;
8
+ constructor(message, status, body) {
9
+ super(message);
10
+ this.status = status;
11
+ this.body = body;
12
+ this.name = 'SolenMonitorError';
13
+ }
14
+ }
15
+ exports.SolenMonitorError = SolenMonitorError;
16
+ function requireNonEmpty(value, field) {
17
+ const trimmed = value.trim();
18
+ if (!trimmed) {
19
+ throw new Error(`SolenMonitor: ${field} is required`);
20
+ }
21
+ return trimmed;
22
+ }
23
+ function errorTitle(error) {
24
+ if (error instanceof Error) {
25
+ return error.message.trim() || error.name || 'Error';
26
+ }
27
+ const text = String(error).trim();
28
+ return text || 'Unknown error';
29
+ }
30
+ function errorDetail(error) {
31
+ if (error instanceof Error && error.stack) {
32
+ return error.stack;
33
+ }
34
+ return undefined;
35
+ }
36
+ function eventTags(tags) {
37
+ if (!tags)
38
+ return undefined;
39
+ const entries = Object.entries(tags).filter(([, v]) => v.trim().length > 0);
40
+ if (entries.length === 0)
41
+ return undefined;
42
+ return entries.map(([k, v]) => `${k}:${v}`);
43
+ }
44
+ function buildSentryWebhookPayload(params) {
45
+ const level = params.level ?? 'error';
46
+ const title = params.title.trim() || 'Incident';
47
+ const message = params.message?.trim() || title;
48
+ const tags = eventTags(params.tags);
49
+ const event = {
50
+ message,
51
+ title,
52
+ level,
53
+ environment: params.environment,
54
+ web_url: params.url,
55
+ url: params.url,
56
+ ...(tags ? { tags } : {}),
57
+ ...(params.extra && Object.keys(params.extra).length > 0
58
+ ? { extra: params.extra }
59
+ : {}),
60
+ };
61
+ return {
62
+ action: 'created',
63
+ installation: { uuid: 'solen-monitor' },
64
+ data: {
65
+ issue: {
66
+ title,
67
+ level,
68
+ permalink: params.url,
69
+ web_url: params.url,
70
+ },
71
+ event,
72
+ },
73
+ level,
74
+ message,
75
+ };
76
+ }
77
+ class SolenMonitor {
78
+ webhookUrl;
79
+ secret;
80
+ constructor(options) {
81
+ this.webhookUrl = requireNonEmpty(options.webhookUrl, 'webhookUrl');
82
+ this.secret = requireNonEmpty(options.secret, 'secret');
83
+ }
84
+ /** Report an exception using a Sentry-compatible webhook payload. */
85
+ async captureException(error, context) {
86
+ const title = errorTitle(error);
87
+ const stack = errorDetail(error);
88
+ await this.send(buildSentryWebhookPayload({
89
+ title,
90
+ message: stack ? `${title}\n\n${stack}` : title,
91
+ level: context?.level ?? 'error',
92
+ environment: context?.environment,
93
+ url: context?.url,
94
+ tags: context?.tags,
95
+ extra: context?.extra,
96
+ }));
97
+ }
98
+ /** Report a message using a Sentry-compatible webhook payload. */
99
+ async captureMessage(message, context) {
100
+ const text = message.trim();
101
+ if (!text) {
102
+ throw new Error('SolenMonitor: message must be non-empty');
103
+ }
104
+ await this.send(buildSentryWebhookPayload({
105
+ title: text,
106
+ message: text,
107
+ level: context?.level ?? 'info',
108
+ environment: context?.environment,
109
+ url: context?.url,
110
+ tags: context?.tags,
111
+ extra: context?.extra,
112
+ }));
113
+ }
114
+ /** POST the payload to the configured webhook. */
115
+ async send(payload) {
116
+ const res = await fetch(this.webhookUrl, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ 'X-Solen-Secret': this.secret,
121
+ },
122
+ body: JSON.stringify(payload),
123
+ });
124
+ const text = await res.text();
125
+ let body = text;
126
+ if (text.length > 0) {
127
+ try {
128
+ body = JSON.parse(text);
129
+ }
130
+ catch {
131
+ body = text;
132
+ }
133
+ }
134
+ else {
135
+ body = {};
136
+ }
137
+ if (!res.ok) {
138
+ const msg = typeof body === 'object' && body && 'error' in body
139
+ ? String(body.error)
140
+ : `HTTP ${res.status}`;
141
+ throw new SolenMonitorError(msg, res.status, body);
142
+ }
143
+ }
144
+ }
145
+ exports.SolenMonitor = SolenMonitor;
@@ -0,0 +1,11 @@
1
+ import type { CaptureContext } from './types.js';
2
+ export type ProcessCapture = {
3
+ captureError: (error: unknown, context?: CaptureContext) => Promise<void>;
4
+ };
5
+ export type RegisterProcessHandlersOptions = {
6
+ environment?: string;
7
+ appName?: string;
8
+ };
9
+ /** Register Node process error handlers that report via {@link ProcessCapture.captureError}. */
10
+ export declare function registerProcessHandlers(capture: ProcessCapture, options?: RegisterProcessHandlersOptions): void;
11
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAoBF,gGAAgG;AAChG,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,8BAA8B,GACvC,IAAI,CAkBN"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerProcessHandlers = registerProcessHandlers;
4
+ function baseTags(appName) {
5
+ const name = appName?.trim();
6
+ return name ? { app: name } : undefined;
7
+ }
8
+ function handlerContext(level, source, options) {
9
+ return {
10
+ level,
11
+ environment: options?.environment,
12
+ tags: baseTags(options?.appName),
13
+ extra: { source },
14
+ };
15
+ }
16
+ /** Register Node process error handlers that report via {@link ProcessCapture.captureError}. */
17
+ function registerProcessHandlers(capture, options) {
18
+ process.on('uncaughtException', (error) => {
19
+ capture
20
+ .captureError(error, handlerContext('critical', 'uncaughtException', options))
21
+ .catch(() => {
22
+ /* reporting must not throw */
23
+ })
24
+ .finally(() => {
25
+ process.exit(1);
26
+ });
27
+ });
28
+ process.on('unhandledRejection', (reason) => {
29
+ void capture.captureError(reason, handlerContext('error', 'unhandledRejection', options));
30
+ });
31
+ }
@@ -0,0 +1,15 @@
1
+ import type { CaptureContext, MonitorOptions } from './types.js';
2
+ export type { CaptureContext, CaptureLevel, MonitorOptions, SentryWebhookData, SentryWebhookEvent, SentryWebhookIssue, SentryWebhookPayload, SolenMonitorOptions, } from './types.js';
3
+ export { buildSentryWebhookPayload, SolenMonitor, SolenMonitorError, } from './client.js';
4
+ export { registerProcessHandlers } from './handlers.js';
5
+ export type { ProcessCapture, RegisterProcessHandlersOptions } from './handlers.js';
6
+ export type MonitorHandle = {
7
+ captureError: (error: unknown, context?: CaptureContext) => Promise<void>;
8
+ captureMessage: (message: string, context?: CaptureContext) => Promise<void>;
9
+ };
10
+ /**
11
+ * Start monitoring: registers `uncaughtException` / `unhandledRejection` handlers
12
+ * and returns manual capture helpers.
13
+ */
14
+ export declare function monitor(options: MonitorOptions): MonitorHandle;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,YAAY,EACV,cAAc,EACd,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,YAAY,EACZ,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,8BAA8B,EAAE,MAAM,eAAe,CAAC;AAEpF,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9E,CAAC;AAUF;;;GAGG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,aAAa,CAsC9D"}
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerProcessHandlers = exports.SolenMonitorError = exports.SolenMonitor = exports.buildSentryWebhookPayload = void 0;
4
+ exports.monitor = monitor;
5
+ const client_js_1 = require("./client.js");
6
+ const handlers_js_1 = require("./handlers.js");
7
+ var client_js_2 = require("./client.js");
8
+ Object.defineProperty(exports, "buildSentryWebhookPayload", { enumerable: true, get: function () { return client_js_2.buildSentryWebhookPayload; } });
9
+ Object.defineProperty(exports, "SolenMonitor", { enumerable: true, get: function () { return client_js_2.SolenMonitor; } });
10
+ Object.defineProperty(exports, "SolenMonitorError", { enumerable: true, get: function () { return client_js_2.SolenMonitorError; } });
11
+ var handlers_js_2 = require("./handlers.js");
12
+ Object.defineProperty(exports, "registerProcessHandlers", { enumerable: true, get: function () { return handlers_js_2.registerProcessHandlers; } });
13
+ function mergeTags(base, extra) {
14
+ if (!base && !extra)
15
+ return undefined;
16
+ return { ...base, ...extra };
17
+ }
18
+ /**
19
+ * Start monitoring: registers `uncaughtException` / `unhandledRejection` handlers
20
+ * and returns manual capture helpers.
21
+ */
22
+ function monitor(options) {
23
+ const client = new client_js_1.SolenMonitor({
24
+ webhookUrl: options.webhookUrl,
25
+ secret: options.secret,
26
+ });
27
+ const defaultTags = options.appName?.trim()
28
+ ? { app: options.appName.trim() }
29
+ : undefined;
30
+ const withDefaults = (context) => ({
31
+ environment: context?.environment ?? options.environment,
32
+ level: context?.level,
33
+ url: context?.url,
34
+ tags: mergeTags(defaultTags, context?.tags),
35
+ extra: context?.extra,
36
+ });
37
+ const captureError = async (error, context) => {
38
+ await client.captureException(error, withDefaults(context));
39
+ };
40
+ const captureMessage = async (message, context) => {
41
+ await client.captureMessage(message, withDefaults(context));
42
+ };
43
+ (0, handlers_js_1.registerProcessHandlers)({ captureError }, { environment: options.environment, appName: options.appName });
44
+ return { captureError, captureMessage };
45
+ }
@@ -0,0 +1,10 @@
1
+ import type { CaptureContext } from '../types.js';
2
+ export type HttpFramework = 'express' | 'fastify';
3
+ export declare function httpErrorCaptureContext(params: {
4
+ framework: HttpFramework;
5
+ route: string;
6
+ method: string;
7
+ status: number;
8
+ }): CaptureContext;
9
+ export declare function statusFromError(err: unknown, responseStatus?: number): number;
10
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/integrations/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,CAAC;AAElD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,cAAc,CAMjB;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAUR"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.httpErrorCaptureContext = httpErrorCaptureContext;
4
+ exports.statusFromError = statusFromError;
5
+ function httpErrorCaptureContext(params) {
6
+ const { framework, route, method, status } = params;
7
+ return {
8
+ level: status >= 500 ? 'error' : 'warning',
9
+ extra: { framework, route, method, status },
10
+ };
11
+ }
12
+ function statusFromError(err, responseStatus) {
13
+ if (typeof responseStatus === 'number' && responseStatus >= 400) {
14
+ return responseStatus;
15
+ }
16
+ if (typeof err === 'object' && err !== null) {
17
+ const record = err;
18
+ if (typeof record.status === 'number')
19
+ return record.status;
20
+ if (typeof record.statusCode === 'number')
21
+ return record.statusCode;
22
+ }
23
+ return 500;
24
+ }
@@ -0,0 +1,11 @@
1
+ import type { ErrorRequestHandler } from 'express';
2
+ import type { CaptureContext } from '../types.js';
3
+ export type SolenCapture = {
4
+ captureError: (error: unknown, context?: CaptureContext) => Promise<void>;
5
+ };
6
+ /**
7
+ * Express error middleware. Reports the error, then calls `next(err)` so
8
+ * downstream handlers can still run.
9
+ */
10
+ export declare function solenErrorHandler(capture: SolenCapture): ErrorRequestHandler;
11
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/integrations/express.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E,CAAC;AAQF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,mBAAmB,CAmB5E"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.solenErrorHandler = solenErrorHandler;
4
+ const context_js_1 = require("./context.js");
5
+ function expressRoute(req) {
6
+ return req.route?.path ?? req.originalUrl ?? req.url ?? req.path ?? '/';
7
+ }
8
+ /**
9
+ * Express error middleware. Reports the error, then calls `next(err)` so
10
+ * downstream handlers can still run.
11
+ */
12
+ function solenErrorHandler(capture) {
13
+ return (err, req, res, next) => {
14
+ const status = (0, context_js_1.statusFromError)(err, res.statusCode);
15
+ const context = (0, context_js_1.httpErrorCaptureContext)({
16
+ framework: 'express',
17
+ route: expressRoute(req),
18
+ method: req.method,
19
+ status,
20
+ });
21
+ void capture
22
+ .captureError(err, context)
23
+ .catch(() => {
24
+ /* reporting must not block downstream handlers */
25
+ })
26
+ .finally(() => {
27
+ next(err);
28
+ });
29
+ };
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+ import type { CaptureContext } from '../types.js';
3
+ export type SolenCapture = {
4
+ captureError: (error: unknown, context?: CaptureContext) => Promise<void>;
5
+ };
6
+ export type SolenFastifyPluginOptions = SolenCapture;
7
+ /**
8
+ * Fastify plugin that wraps `setErrorHandler`, reports errors, then delegates
9
+ * to any previously registered error handler.
10
+ */
11
+ export declare const solenFastifyPlugin: FastifyPluginAsync<SolenFastifyPluginOptions>;
12
+ //# sourceMappingURL=fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/integrations/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAGnB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,YAAY,CAAC;AAwBrD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,kBAAkB,CAAC,yBAAyB,CA8B1E,CAAC"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.solenFastifyPlugin = void 0;
4
+ const context_js_1 = require("./context.js");
5
+ function fastifyRoute(request) {
6
+ return request.routeOptions?.url ?? request.url ?? '/';
7
+ }
8
+ function asFastifyError(error) {
9
+ if (typeof error === 'object' &&
10
+ error !== null &&
11
+ 'message' in error &&
12
+ typeof error.message === 'string') {
13
+ return error;
14
+ }
15
+ const wrapped = new Error(String(error));
16
+ wrapped.statusCode = 500;
17
+ return wrapped;
18
+ }
19
+ function fastifyStatus(error, reply) {
20
+ return (0, context_js_1.statusFromError)(error, reply.statusCode);
21
+ }
22
+ /**
23
+ * Fastify plugin that wraps `setErrorHandler`, reports errors, then delegates
24
+ * to any previously registered error handler.
25
+ */
26
+ const solenFastifyPlugin = async (fastify, opts) => {
27
+ const previous = fastify.errorHandler;
28
+ fastify.setErrorHandler(async (error, request, reply) => {
29
+ const err = asFastifyError(error);
30
+ const status = fastifyStatus(err, reply);
31
+ const context = (0, context_js_1.httpErrorCaptureContext)({
32
+ framework: 'fastify',
33
+ route: fastifyRoute(request),
34
+ method: request.method,
35
+ status,
36
+ });
37
+ try {
38
+ await opts.captureError(err, context);
39
+ }
40
+ catch {
41
+ /* reporting must not block downstream handlers */
42
+ }
43
+ if (previous) {
44
+ return previous.call(fastify, err, request, reply);
45
+ }
46
+ reply.status(status).send({
47
+ statusCode: status,
48
+ error: err.name ?? 'Error',
49
+ message: err.message,
50
+ });
51
+ });
52
+ };
53
+ exports.solenFastifyPlugin = solenFastifyPlugin;
@@ -0,0 +1,57 @@
1
+ /** Sentry issue webhook shape (subset) recognized by MetaShift `detectWebhookSource`. */
2
+ export interface SentryWebhookIssue {
3
+ title?: string;
4
+ culprit?: string;
5
+ permalink?: string;
6
+ web_url?: string;
7
+ level?: string;
8
+ }
9
+ /** Sentry event webhook shape (subset) recognized by MetaShift `normalizeSentry`. */
10
+ export interface SentryWebhookEvent {
11
+ message?: string;
12
+ title?: string;
13
+ level?: string;
14
+ environment?: string;
15
+ web_url?: string;
16
+ url?: string;
17
+ tags?: string[] | Record<string, string>;
18
+ extra?: Record<string, unknown>;
19
+ }
20
+ export interface SentryWebhookData {
21
+ issue?: SentryWebhookIssue;
22
+ event?: SentryWebhookEvent;
23
+ project?: string;
24
+ }
25
+ /** Outbound payload mimicking a Sentry integration webhook for MetaShift normalization. */
26
+ export interface SentryWebhookPayload {
27
+ action: string;
28
+ installation?: {
29
+ uuid?: string;
30
+ };
31
+ data: SentryWebhookData;
32
+ level?: string;
33
+ message?: string;
34
+ event?: SentryWebhookEvent;
35
+ }
36
+ export type SolenMonitorOptions = {
37
+ /** MetaShift incident webhook URL (`/api/webhooks/incident/:endpointId`). */
38
+ webhookUrl: string;
39
+ /** Shared secret sent as `X-Solen-Secret`. */
40
+ secret: string;
41
+ };
42
+ export type CaptureLevel = 'fatal' | 'critical' | 'error' | 'warning' | 'info' | 'debug';
43
+ export type MonitorOptions = {
44
+ webhookUrl: string;
45
+ secret: string;
46
+ environment?: string;
47
+ appName?: string;
48
+ };
49
+ export type CaptureContext = {
50
+ environment?: string;
51
+ level?: CaptureLevel;
52
+ /** Issue/event permalink shown on the incident. */
53
+ url?: string;
54
+ tags?: Record<string, string>;
55
+ extra?: Record<string, unknown>;
56
+ };
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,2FAA2F;AAC3F,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB,OAAO,GACP,UAAU,GACV,OAAO,GACP,SAAS,GACT,MAAM,GACN,OAAO,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@solenai/monitor",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight incident reporter for Solen MetaShift webhooks (Sentry-compatible payload)",
5
+ "license": "MIT",
6
+ "author": "Solen",
7
+ "keywords": [
8
+ "solen",
9
+ "monitor",
10
+ "webhook",
11
+ "sentry",
12
+ "incident"
13
+ ],
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./express": {
22
+ "types": "./dist/integrations/express.d.ts",
23
+ "default": "./dist/integrations/express.js"
24
+ },
25
+ "./fastify": {
26
+ "types": "./dist/integrations/fastify.d.ts",
27
+ "default": "./dist/integrations/fastify.js"
28
+ }
29
+ },
30
+ "peerDependencies": {
31
+ "express": ">=4.0.0",
32
+ "fastify": ">=4.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "express": {
36
+ "optional": true
37
+ },
38
+ "fastify": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "README.md"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc",
51
+ "typecheck": "tsc --noEmit"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ },
56
+ "devDependencies": {
57
+ "@types/express": "^4.17.21",
58
+ "@types/node": "^20.10.0",
59
+ "fastify": "^5.8.1",
60
+ "typescript": "^5.3.3"
61
+ }
62
+ }