@sigx/lynx-observability 0.5.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Andreas Ekdahl
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,103 @@
1
+ # @sigx/lynx-observability
2
+
3
+ Opt-in **production error capture** and **provider-agnostic log/error sinks** for sigx-lynx. Builds on the logger in [`@sigx/lynx-core`](https://github.com/signalxjs/lynx/tree/main/packages/lynx-core#logging): uncaught errors are funneled in as `error`-level records, and a "sink" is just a `LogTransport`. No hard dependency on any vendor SDK.
4
+
5
+ > Logging itself ships in the framework (`import { createLogger } from '@sigx/lynx'`). This package adds the *production* pieces — catching crashes and shipping records off-device — and is installed only when you want them.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @sigx/lynx-observability
11
+ ```
12
+
13
+ ## Quick start — declarative (recommended)
14
+
15
+ Declare it in `signalx.config.ts` and it auto-wires in **release** builds — no code in your app entry:
16
+
17
+ ```ts
18
+ // signalx.config.ts
19
+ export default defineLynxConfig({
20
+ name: 'my-app',
21
+ logging: {
22
+ level: 'warn', // logger level (also dev: 'debug' / release: 'warn' default)
23
+ namespaces: { disabled: ['http'] },// silence namespaces at startup
24
+ production: {
25
+ sink: { url: 'https://logs.example.com/ingest', headers: { 'x-api-key': KEY }, sampleRate: 0.25 },
26
+ captureErrors: true, // default
27
+ },
28
+ },
29
+ });
30
+ ```
31
+
32
+ Just install the package (`pnpm add @sigx/lynx-observability`) — `@sigx/lynx-plugin` prepends the init for you in release builds. (Dev uses the console streamer; observability auto-wiring is release-only.)
33
+
34
+ ## Quick start — manual
35
+
36
+ Or wire it yourself, `Sentry.init()`-style (call once in your app entry):
37
+
38
+ ```ts
39
+ import { initObservability } from '@sigx/lynx-observability';
40
+
41
+ initObservability({
42
+ level: 'warn', // optional: override the default level
43
+ captureErrors: true, // default — catch uncaught errors / rejections
44
+ sink: { // optional remote sink
45
+ url: 'https://logs.example.com/ingest',
46
+ headers: { 'x-api-key': API_KEY },
47
+ sampleRate: 0.25, // keep 25% of non-error records; errors always kept
48
+ },
49
+ });
50
+ ```
51
+
52
+ That's it: uncaught errors now flow to your logs (and the `sigx dev` terminal in development), and records at/above the level are batched and POSTed to your endpoint.
53
+
54
+ ## Pieces (compose them yourself)
55
+
56
+ - **`installErrorCapture(opts?)`** — registers Lynx's `lynx.onError` (background thread) plus `globalThis` `error`/`unhandledrejection` handlers, normalizes whatever was thrown into an `Error`, and logs it at `error` level under the `uncaught` namespace. The `Error` rides in the record's `fields`, so transports can treat it as an exception (with a stack). Idempotent; returns an uninstall function.
57
+ - **`createHttpSink(opts)`** — a batching `LogTransport` that POSTs `{ records: [...] }` as JSON to `opts.url`. Options: `batchSize`, `flushIntervalMs`, `sampleRate`, `minLevel`, `headers`, `excludeNamespaces`. `Error` fields are serialized to `{ name, message, stack }`. It excludes the `http` namespace by default (its own POSTs log there) and swallows its own send failures, so it can't feed back into itself. Has a `.flush()` for graceful shutdown.
58
+ - **`toError(value)`** — the normalization helper, exported for reuse.
59
+
60
+ ```ts
61
+ import { addTransport } from '@sigx/lynx';
62
+ import { createHttpSink, installErrorCapture } from '@sigx/lynx-observability';
63
+
64
+ addTransport(createHttpSink({ url, minLevel: 'info' }));
65
+ const uninstall = installErrorCapture({ onError: (e) => myAnalytics.track('crash', e.message) });
66
+ ```
67
+
68
+ ## Wire format
69
+
70
+ The sink POSTs:
71
+
72
+ ```json
73
+ { "records": [ { "level": "error", "namespace": "uncaught", "msg": "[lynx] …", "fields": [ { "name": "TypeError", "message": "…", "stack": "…" } ], "ts": 1733740000000 } ] }
74
+ ```
75
+
76
+ ## Provider adapters
77
+
78
+ There's no built-in vendor coupling — any provider is a `LogTransport`. Errors arrive as `error`-level records with the `Error` in `fields[0]`, so an adapter can split exceptions from breadcrumbs. Example **Sentry** adapter (Sentry is an optional peer in *your* app, not a dependency of this package):
79
+
80
+ ```ts
81
+ import * as Sentry from '@sentry/browser'; // your app's dep
82
+ import { addTransport, installErrorCapture, type LogRecord } from '@sigx/lynx';
83
+
84
+ Sentry.init({ dsn: SENTRY_DSN });
85
+
86
+ addTransport((r: LogRecord) => {
87
+ const err = r.fields.find((f) => f instanceof Error) as Error | undefined;
88
+ if (r.level.name === 'error' && err) {
89
+ Sentry.captureException(err);
90
+ } else {
91
+ Sentry.addBreadcrumb({ category: r.namespace, message: r.msg, level: r.level.name });
92
+ }
93
+ });
94
+ installErrorCapture();
95
+ ```
96
+
97
+ The same shape works for Datadog, a custom backend, etc.
98
+
99
+ ## Notes
100
+
101
+ - `lynx.onError` is **background-thread only** upstream; main-thread error capture may need a separate path in the future.
102
+ - For readable stack traces in release builds, upload your source maps to your provider (out of scope here).
103
+ - Declarative `signalx.config.ts` `logging.production` config auto-wires this package in release builds (see Quick start above); `initObservability()` remains for manual/dev setup.
@@ -0,0 +1,13 @@
1
+ export interface ErrorCaptureOptions {
2
+ /** Extra callback invoked with the normalized Error for each captured error. */
3
+ onError?: (error: Error) => void;
4
+ }
5
+ /** Normalize anything thrown (Error, ErrorEvent, PromiseRejection, string, …) into an Error. */
6
+ export declare function toError(input: unknown): Error;
7
+ /**
8
+ * Install global error capture. Returns an uninstall function. Idempotent —
9
+ * calling it again while already installed is a no-op and returns a no-op
10
+ * uninstall.
11
+ */
12
+ export declare function installErrorCapture(opts?: ErrorCaptureOptions): () => void;
13
+ //# sourceMappingURL=error-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-capture.d.ts","sourceRoot":"","sources":["../src/error-capture.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,mBAAmB;IAChC,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACpC;AAUD,gGAAgG;AAChG,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAa7C;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,mBAAwB,GAAG,MAAM,IAAI,CAmE9E"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Production error capture. Registers Lynx's background-thread error hook
3
+ * (`lynx.onError`) plus the `globalThis` `error` / `unhandledrejection`
4
+ * handlers where present, normalizes whatever was thrown into an `Error`, and
5
+ * funnels it into the core logger as an `error`-level record under the
6
+ * `uncaught` namespace.
7
+ *
8
+ * Because it goes through the core logger, captured errors show up in the
9
+ * `sigx dev` terminal AND reach every registered transport (e.g. a remote
10
+ * sink from {@link createHttpSink}). The original `Error` rides in the record
11
+ * `fields`, so transports can treat it as an exception (with a stack) rather
12
+ * than a plain log line.
13
+ */
14
+ import { createLogger } from '@sigx/lynx-core';
15
+ const log = createLogger('uncaught');
16
+ function lynxObj() {
17
+ return typeof lynx !== 'undefined' ? lynx : undefined;
18
+ }
19
+ function safeStringify(value) {
20
+ try {
21
+ return JSON.stringify(value) ?? String(value);
22
+ }
23
+ catch {
24
+ return String(value);
25
+ }
26
+ }
27
+ /** Normalize anything thrown (Error, ErrorEvent, PromiseRejection, string, …) into an Error. */
28
+ export function toError(input) {
29
+ if (input instanceof Error)
30
+ return input;
31
+ if (input && typeof input === 'object') {
32
+ const o = input;
33
+ // ErrorEvent.error / PromiseRejectionEvent.reason may hold the real Error.
34
+ const inner = o['error'] ?? o['reason'];
35
+ if (inner instanceof Error)
36
+ return inner;
37
+ const msg = o['message'] ?? o['reason'] ?? o['error'];
38
+ const err = new Error(typeof msg === 'string' && msg ? msg : safeStringify(input));
39
+ if (typeof o['stack'] === 'string')
40
+ err.stack = o['stack'];
41
+ return err;
42
+ }
43
+ return new Error(typeof input === 'string' ? input : safeStringify(input));
44
+ }
45
+ // Idempotent across module re-evaluation (HMR) / multiple bundle copies.
46
+ const G = globalThis;
47
+ const INSTALLED = '__sigxObservabilityErrorCaptureInstalled';
48
+ /**
49
+ * Install global error capture. Returns an uninstall function. Idempotent —
50
+ * calling it again while already installed is a no-op and returns a no-op
51
+ * uninstall.
52
+ */
53
+ export function installErrorCapture(opts = {}) {
54
+ if (G[INSTALLED])
55
+ return () => { };
56
+ G[INSTALLED] = true;
57
+ const report = (input, source) => {
58
+ const err = toError(input);
59
+ // Error object in fields → transports can treat it as an exception.
60
+ log.error(`[${source}] ${err.message}`, err);
61
+ try {
62
+ opts.onError?.(err);
63
+ }
64
+ catch {
65
+ /* never let a user hook break capture */
66
+ }
67
+ };
68
+ const undo = [];
69
+ // 1. Lynx background-thread hook (no documented removal — nothing to undo).
70
+ const lx = lynxObj();
71
+ if (typeof lx?.onError === 'function') {
72
+ lx.onError((e) => report(e, 'lynx'));
73
+ }
74
+ // 2. globalThis handlers (web/dev and any host exposing them).
75
+ const g = globalThis;
76
+ if (typeof g.addEventListener === 'function') {
77
+ const onErr = (e) => report(e?.error ?? e, 'error');
78
+ const onRej = (e) => report(e?.reason ?? e, 'unhandledrejection');
79
+ g.addEventListener('error', onErr);
80
+ g.addEventListener('unhandledrejection', onRej);
81
+ undo.push(() => {
82
+ g.removeEventListener?.('error', onErr);
83
+ g.removeEventListener?.('unhandledrejection', onRej);
84
+ });
85
+ }
86
+ else {
87
+ const prevErr = g.onerror;
88
+ const prevRej = g.onunhandledrejection;
89
+ g.onerror = (...args) => {
90
+ // (message, source, lineno, colno, error)
91
+ report(args[4] ?? args[0], 'error');
92
+ // Chain to any pre-existing handler so we don't clobber host/app
93
+ // error reporting; preserve its return value (truthy = handled).
94
+ if (typeof prevErr === 'function') {
95
+ return prevErr(...args);
96
+ }
97
+ return false;
98
+ };
99
+ g.onunhandledrejection = (e) => {
100
+ report(e?.reason ?? e, 'unhandledrejection');
101
+ if (typeof prevRej === 'function')
102
+ prevRej(e);
103
+ };
104
+ undo.push(() => {
105
+ g.onerror = prevErr;
106
+ g.onunhandledrejection = prevRej;
107
+ });
108
+ }
109
+ return () => {
110
+ if (!G[INSTALLED])
111
+ return;
112
+ G[INSTALLED] = false;
113
+ for (const u of undo)
114
+ u();
115
+ };
116
+ }
117
+ //# sourceMappingURL=error-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-capture.js","sourceRoot":"","sources":["../src/error-capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { LogLevelName, LogTransport } from '@sigx/lynx-core';
2
+ export interface HttpSinkOptions {
3
+ /** Endpoint that receives `POST` `{ records: WireRecord[] }`. */
4
+ url: string;
5
+ /** Extra headers (e.g. auth). `content-type: application/json` is set for you. */
6
+ headers?: Record<string, string>;
7
+ /** Flush once the buffer reaches this many records. Default 20. */
8
+ batchSize?: number;
9
+ /** Flush at most this often (ms) while records trickle in. Default 5000. */
10
+ flushIntervalMs?: number;
11
+ /** Keep this fraction (0–1) of non-error records; errors are always kept. Default 1. */
12
+ sampleRate?: number;
13
+ /** Only send records at or above this level. Default `'info'`. */
14
+ minLevel?: LogLevelName;
15
+ /** Namespaces to drop. Default `['http']` (prevents the sink's own POSTs feeding back). */
16
+ excludeNamespaces?: string[];
17
+ }
18
+ /** A LogTransport with an extra `flush()` for tests / graceful shutdown. */
19
+ export type HttpSink = LogTransport & {
20
+ flush(): void;
21
+ };
22
+ export declare function createHttpSink(opts: HttpSinkOptions): HttpSink;
23
+ //# sourceMappingURL=http-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-sink.d.ts","sourceRoot":"","sources":["../src/http-sink.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAa,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,eAAe;IAC5B,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAUD,4EAA4E;AAC5E,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAAC;AAOxD,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CAoD9D"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Provider-agnostic batching HTTP sink — a {@link LogTransport} that buffers
3
+ * log records and POSTs them as JSON to any endpoint (your own backend, an
4
+ * OTLP-style collector, a serverless function, …). Register it with the core
5
+ * logger's `addTransport`.
6
+ *
7
+ * Loop safety: the sink's own POST goes through `@sigx/lynx-http`, which logs
8
+ * under the `http` namespace — so that namespace is excluded by default to
9
+ * stop the sink from feeding its own traffic back into itself. The sink also
10
+ * swallows its own send failures (it never logs them).
11
+ */
12
+ import { fetch } from '@sigx/lynx-http';
13
+ const SEVERITY = {
14
+ trace: 10, debug: 20, info: 30, warn: 40, error: 50, silent: 100,
15
+ };
16
+ function serializeField(f) {
17
+ if (f instanceof Error)
18
+ return { name: f.name, message: f.message, stack: f.stack };
19
+ return f;
20
+ }
21
+ export function createHttpSink(opts) {
22
+ const batchSize = opts.batchSize ?? 20;
23
+ const flushIntervalMs = opts.flushIntervalMs ?? 5000;
24
+ const sampleRate = opts.sampleRate ?? 1;
25
+ const minSeverity = SEVERITY[opts.minLevel ?? 'info'];
26
+ const exclude = new Set(opts.excludeNamespaces ?? ['http']);
27
+ let buffer = [];
28
+ let timer = null;
29
+ const flush = () => {
30
+ if (timer !== null) {
31
+ clearTimeout(timer);
32
+ timer = null;
33
+ }
34
+ if (buffer.length === 0)
35
+ return;
36
+ const batch = buffer;
37
+ buffer = [];
38
+ // Fire-and-forget; swallow failures so the sink never feeds its own
39
+ // errors back into the logger (which would loop into this transport).
40
+ void fetch(opts.url, {
41
+ method: 'POST',
42
+ // content-type is set for us — caller headers can't override it.
43
+ headers: { ...opts.headers, 'content-type': 'application/json' },
44
+ body: JSON.stringify({ records: batch }),
45
+ }).catch(() => { });
46
+ };
47
+ const schedule = () => {
48
+ if (timer === null)
49
+ timer = setTimeout(flush, flushIntervalMs);
50
+ };
51
+ const sink = ((record) => {
52
+ if (record.level.severity < minSeverity)
53
+ return;
54
+ if (exclude.has(record.namespace))
55
+ return;
56
+ // Sample non-error records; always keep errors.
57
+ if (sampleRate < 1 && record.level.severity < SEVERITY.error && Math.random() >= sampleRate) {
58
+ return;
59
+ }
60
+ buffer.push({
61
+ level: record.level.name,
62
+ namespace: record.namespace,
63
+ msg: record.msg,
64
+ fields: record.fields.map(serializeField),
65
+ ts: record.ts,
66
+ });
67
+ if (buffer.length >= batchSize)
68
+ flush();
69
+ else
70
+ schedule();
71
+ });
72
+ sink.flush = flush;
73
+ return sink;
74
+ }
75
+ //# sourceMappingURL=http-sink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-sink.js","sourceRoot":"","sources":["../src/http-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAGxC,MAAM,QAAQ,GAAiC;IAC3C,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG;CACnE,CAAC;AA8BF,SAAS,cAAc,CAAC,CAAU;IAC9B,IAAI,CAAC,YAAY,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACpF,OAAO,CAAC,CAAC;AACb,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAqB;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;IACvC,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC;IACrD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;IACxC,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAE5D,IAAI,MAAM,GAAiB,EAAE,CAAC;IAC9B,IAAI,KAAK,GAAyC,IAAI,CAAC;IAEvD,MAAM,KAAK,GAAG,GAAS,EAAE;QACrB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACjB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAChC,MAAM,KAAK,GAAG,MAAM,CAAC;QACrB,MAAM,GAAG,EAAE,CAAC;QACZ,oEAAoE;QACpE,sEAAsE;QACtE,KAAK,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE;YACjB,MAAM,EAAE,MAAM;YACd,iEAAiE;YACjE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAChE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;SAC3C,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAiB,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAS,EAAE;QACxB,IAAI,KAAK,KAAK,IAAI;YAAE,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IACnE,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,CAAC,MAAiB,EAAQ,EAAE;QACtC,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,WAAW;YAAE,OAAO;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC;YAAE,OAAO;QAC1C,gDAAgD;QAChD,IAAI,UAAU,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;YAC1F,OAAO;QACX,CAAC;QACD,MAAM,CAAC,IAAI,CAAC;YACR,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;YACzC,EAAE,EAAE,MAAM,CAAC,EAAE;SAChB,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,IAAI,SAAS;YAAE,KAAK,EAAE,CAAC;;YACnC,QAAQ,EAAE,CAAC;IACpB,CAAC,CAAa,CAAC;IAEf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACnB,OAAO,IAAI,CAAC;AAChB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @sigx/lynx-observability — opt-in production error capture + provider-agnostic
3
+ * log/error sinks for sigx-lynx.
4
+ *
5
+ * Builds on the `@sigx/lynx-core` logger: uncaught errors are funneled in as
6
+ * `error`-level records, and sinks are just `LogTransport`s. Call
7
+ * {@link initObservability} once in your app entry, or compose the pieces
8
+ * ({@link installErrorCapture}, {@link createHttpSink}) yourself.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { initObservability } from './init.js';
13
+ export type { ObservabilityOptions } from './init.js';
14
+ export { installErrorCapture, toError } from './error-capture.js';
15
+ export type { ErrorCaptureOptions } from './error-capture.js';
16
+ export { createHttpSink } from './http-sink.js';
17
+ export type { HttpSink, HttpSinkOptions } from './http-sink.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @sigx/lynx-observability — opt-in production error capture + provider-agnostic
3
+ * log/error sinks for sigx-lynx.
4
+ *
5
+ * Builds on the `@sigx/lynx-core` logger: uncaught errors are funneled in as
6
+ * `error`-level records, and sinks are just `LogTransport`s. Call
7
+ * {@link initObservability} once in your app entry, or compose the pieces
8
+ * ({@link installErrorCapture}, {@link createHttpSink}) yourself.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { initObservability } from './init.js';
13
+ export { installErrorCapture, toError } from './error-capture.js';
14
+ export { createHttpSink } from './http-sink.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/init.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One-call setup, à la `Sentry.init()`. Call once in your app entry.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { initObservability } from '@sigx/lynx-observability';
7
+ *
8
+ * initObservability({
9
+ * level: 'warn', // optional: override the default level
10
+ * captureErrors: true, // default: capture uncaught errors
11
+ * sink: { url: 'https://logs.example.com/ingest', headers: { 'x-api-key': KEY } },
12
+ * });
13
+ * ```
14
+ */
15
+ import { type LogLevelName } from '@sigx/lynx-core';
16
+ import { type HttpSinkOptions } from './http-sink.js';
17
+ import { type ErrorCaptureOptions } from './error-capture.js';
18
+ export interface ObservabilityOptions {
19
+ /** Override the global log level (e.g. `'warn'` in production). */
20
+ level?: LogLevelName;
21
+ /** Remote sink to forward records to. Omit for error-capture only. */
22
+ sink?: HttpSinkOptions;
23
+ /** Capture uncaught errors / unhandled rejections. Default `true`. */
24
+ captureErrors?: boolean;
25
+ /** Options for {@link installErrorCapture} (e.g. an extra `onError` hook). */
26
+ errorCapture?: ErrorCaptureOptions;
27
+ }
28
+ /** Wire up logging level, an optional remote sink, and error capture in one call. */
29
+ export declare function initObservability(opts?: ObservabilityOptions): void;
30
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAA6B,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEnF,MAAM,WAAW,oBAAoB;IACjC,mEAAmE;IACnE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,sEAAsE;IACtE,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,sEAAsE;IACtE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,oBAAyB,GAAG,IAAI,CAIvE"}
package/dist/init.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * One-call setup, à la `Sentry.init()`. Call once in your app entry.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { initObservability } from '@sigx/lynx-observability';
7
+ *
8
+ * initObservability({
9
+ * level: 'warn', // optional: override the default level
10
+ * captureErrors: true, // default: capture uncaught errors
11
+ * sink: { url: 'https://logs.example.com/ingest', headers: { 'x-api-key': KEY } },
12
+ * });
13
+ * ```
14
+ */
15
+ import { addTransport, setLogLevel } from '@sigx/lynx-core';
16
+ import { createHttpSink } from './http-sink.js';
17
+ import { installErrorCapture } from './error-capture.js';
18
+ /** Wire up logging level, an optional remote sink, and error capture in one call. */
19
+ export function initObservability(opts = {}) {
20
+ if (opts.level)
21
+ setLogLevel(opts.level);
22
+ if (opts.sink)
23
+ addTransport(createHttpSink(opts.sink));
24
+ if (opts.captureErrors !== false)
25
+ installErrorCapture(opts.errorCapture);
26
+ }
27
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAqB,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAwB,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAA4B,MAAM,oBAAoB,CAAC;AAanF,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,IAAI,GAAyB,EAAE;IAC7D,IAAI,IAAI,CAAC,KAAK;QAAE,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,IAAI,CAAC,IAAI;QAAE,YAAY,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,IAAI,IAAI,CAAC,aAAa,KAAK,KAAK;QAAE,mBAAmB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAC7E,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { HttpSinkOptions } from './http-sink.js';
2
+ /** Config injected into {@link install} (the `logging.production` shape). */
3
+ export interface InjectedObservabilityConfig {
4
+ sink?: HttpSinkOptions;
5
+ captureErrors?: boolean;
6
+ }
7
+ /**
8
+ * Apply the injected config: no-op when `null`/`undefined`, otherwise wire up
9
+ * observability. Never throws — observability must not crash the host app.
10
+ * Exported (not just the side effect) so it's unit-testable.
11
+ */
12
+ export declare function install(cfg: InjectedObservabilityConfig | null | undefined): void;
13
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,6EAA6E;AAC7E,MAAM,WAAW,2BAA2B;IACxC,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,2BAA2B,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAMjF"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Auto-installed by `@sigx/lynx-plugin` in **release** builds when the app's
3
+ * `signalx.config.ts` declares `logging.production`. Prepended to the BG entry
4
+ * so error capture + the remote sink are wired before app code runs.
5
+ *
6
+ * Never import this directly from app code — it has unconditional side effects.
7
+ * To set up observability manually instead, call `initObservability(...)`.
8
+ */
9
+ import { initObservability } from './init.js';
10
+ /**
11
+ * Apply the injected config: no-op when `null`/`undefined`, otherwise wire up
12
+ * observability. Never throws — observability must not crash the host app.
13
+ * Exported (not just the side effect) so it's unit-testable.
14
+ */
15
+ export function install(cfg) {
16
+ try {
17
+ if (cfg)
18
+ initObservability({ sink: cfg.sink, captureErrors: cfg.captureErrors });
19
+ }
20
+ catch {
21
+ /* never let observability wiring crash the host app */
22
+ }
23
+ }
24
+ install(typeof __SIGX_OBSERVABILITY_CONFIG__ !== 'undefined' ? __SIGX_OBSERVABILITY_CONFIG__ : null);
25
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAS9C;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,GAAmD;IACvE,IAAI,CAAC;QACD,IAAI,GAAG;YAAE,iBAAiB,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACL,uDAAuD;IAC3D,CAAC;AACL,CAAC;AAMD,OAAO,CAAC,OAAO,6BAA6B,KAAK,WAAW,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@sigx/lynx-observability",
3
+ "version": "0.5.2",
4
+ "description": "Opt-in production error capture + provider-agnostic log/error sinks for sigx-lynx",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./install": {
14
+ "import": "./dist/install.js",
15
+ "types": "./dist/install.d.ts"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "sigx",
24
+ "lynx",
25
+ "logging",
26
+ "observability",
27
+ "error-reporting"
28
+ ],
29
+ "author": "Andreas Ekdahl",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/signalxjs/lynx.git",
34
+ "directory": "packages/lynx-observability"
35
+ },
36
+ "homepage": "https://github.com/signalxjs/lynx/tree/main/packages/lynx-observability",
37
+ "bugs": {
38
+ "url": "https://github.com/signalxjs/lynx/issues"
39
+ },
40
+ "dependencies": {
41
+ "@sigx/lynx-core": "^0.5.2",
42
+ "@sigx/lynx-http": "^0.5.2"
43
+ },
44
+ "devDependencies": {
45
+ "@typescript/native-preview": "7.0.0-dev.20260521.1",
46
+ "typescript": "^6.0.3"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "build": "node ../../scripts/clean.mjs dist && tsgo",
53
+ "dev": "tsgo --watch",
54
+ "clean": "node ../../scripts/clean.mjs dist .turbo"
55
+ }
56
+ }