@tallyrow/safesignal 1.2.0 → 1.4.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 +359 -22
- package/dist/capture.cjs +77 -0
- package/dist/capture.cjs.map +1 -0
- package/dist/capture.d.cts +49 -0
- package/dist/capture.d.ts +49 -0
- package/dist/capture.mjs +75 -0
- package/dist/capture.mjs.map +1 -0
- package/dist/dev-console.cjs +90 -0
- package/dist/dev-console.cjs.map +1 -0
- package/dist/dev-console.d.cts +67 -0
- package/dist/dev-console.d.ts +67 -0
- package/dist/dev-console.mjs +88 -0
- package/dist/dev-console.mjs.map +1 -0
- package/dist/framework-react.cjs +92 -0
- package/dist/framework-react.cjs.map +1 -0
- package/dist/framework-react.d.cts +97 -0
- package/dist/framework-react.d.ts +97 -0
- package/dist/framework-react.mjs +87 -0
- package/dist/framework-react.mjs.map +1 -0
- package/dist/framework-vue.cjs +88 -0
- package/dist/framework-vue.cjs.map +1 -0
- package/dist/framework-vue.d.cts +101 -0
- package/dist/framework-vue.d.ts +101 -0
- package/dist/framework-vue.mjs +82 -0
- package/dist/framework-vue.mjs.map +1 -0
- package/dist/index.cjs +180 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +180 -40
- package/dist/index.mjs.map +1 -1
- package/dist/stacks.cjs +81 -0
- package/dist/stacks.cjs.map +1 -0
- package/dist/stacks.d.cts +55 -0
- package/dist/stacks.d.ts +55 -0
- package/dist/stacks.mjs +77 -0
- package/dist/stacks.mjs.map +1 -0
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/transport-beacon.cjs.map +1 -1
- package/dist/transport-beacon.d.cts +1 -1
- package/dist/transport-beacon.d.ts +1 -1
- package/dist/transport-beacon.mjs.map +1 -1
- package/dist/transport-otlp.cjs +84 -4
- package/dist/transport-otlp.cjs.map +1 -1
- package/dist/transport-otlp.d.cts +10 -1
- package/dist/transport-otlp.d.ts +10 -1
- package/dist/transport-otlp.mjs +84 -4
- package/dist/transport-otlp.mjs.map +1 -1
- package/dist/{types-BiRyHi1e.d.cts → types-CZtSjgq5.d.cts} +53 -1
- package/dist/{types-BiRyHi1e.d.ts → types-CZtSjgq5.d.ts} +53 -1
- package/package.json +58 -7
package/README.md
CHANGED
|
@@ -2,11 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
**SafeSignal** is a browser-first, vendor-neutral structured logging
|
|
4
4
|
facade and safety boundary for browser applications and federated
|
|
5
|
-
frontend modules.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
frontend modules. It catches the errors your users actually hit —
|
|
6
|
+
uncaught exceptions, unhandled rejections, and React/Vue component
|
|
7
|
+
crashes — and ships them securely to any backend. Secure-by-default
|
|
8
|
+
sanitization, URL scrubbing, key + shape redaction, control-character
|
|
9
|
+
escaping, and a pluggable transport boundary — all applied before any
|
|
10
|
+
transport sees an event. Published on npm as `@tallyrow/safesignal`
|
|
11
|
+
(TallyRow is the publishing organization; SafeSignal is the product).
|
|
12
|
+
|
|
13
|
+
## What you get
|
|
14
|
+
|
|
15
|
+
The visible wins — all opt-in subpaths, each routed through the same
|
|
16
|
+
secure pipeline as any log:
|
|
17
|
+
|
|
18
|
+
- ⭐ **[Catch the silent errors](#catch-uncaught-errors--capture-subpath)** —
|
|
19
|
+
the opt-in `./capture` subpath logs the **uncaught exceptions and
|
|
20
|
+
unhandled rejections** that otherwise never reach your transports.
|
|
21
|
+
- **[Dev-mode console rendering](#pretty-dev-logs--dev-console-subpath)** —
|
|
22
|
+
readable, colorized log output in development.
|
|
23
|
+
- **[Error breadcrumbs](#error-breadcrumbs--recent-event-context-on-errors)** —
|
|
24
|
+
bounded recent-event context attached to each error.
|
|
25
|
+
- **[Readable, source-mapped error stacks](#readable-error-stacks--stacks-subpath)** —
|
|
26
|
+
turn a wall of minified frames into trimmed, structured ones.
|
|
27
|
+
- **[React error boundary + hook](#catch-react-errors--framework-react-subpath)** —
|
|
28
|
+
`./framework-react` routes component-tree errors through your `Logger`.
|
|
29
|
+
- **[Vue errorHandler adapter](#catch-vue-errors--framework-vue-subpath)** —
|
|
30
|
+
`./framework-vue` does the same for Vue 3.
|
|
31
|
+
|
|
32
|
+
The core stays tiny and ships nothing you don't import. Here is *why*
|
|
33
|
+
each of those stays safe:
|
|
10
34
|
|
|
11
35
|
## Why SafeSignal
|
|
12
36
|
|
|
@@ -14,7 +38,7 @@ publishing organization; SafeSignal is the product).
|
|
|
14
38
|
- **Never-throw boundary**: no transport, redactor, or sanitizer failure propagates into your `log.info(...)` call site. Logging cannot break rendering, navigation, or state updates.
|
|
15
39
|
- **Vendor-neutral transport**: ship to Datadog, Honeycomb, your own ingestion, or the built-in `./transport-beacon` subpath for body-only HTTPS delivery — same API regardless of destination.
|
|
16
40
|
- **Federated-runtime aware**: host owns the configured runtime; modules import loggers without re-configuring. Hundreds of `Logger` instances per page stay constant-cost.
|
|
17
|
-
- **Lightweight**: ~8 KB gzipped default entry; structured events with bounded depth and bounded size; no global listeners
|
|
41
|
+
- **Lightweight**: ~8 KB gzipped default entry; structured events with bounded depth and bounded size; the core installs no global listeners and reads no ambient state (an opt-in host subpath may install one — see the [`./capture` subpath](#catch-uncaught-errors--capture-subpath)), and `Logger` creation does no per-instance backend init.
|
|
18
42
|
|
|
19
43
|
## Install
|
|
20
44
|
|
|
@@ -48,9 +72,15 @@ log.info('checkout opened', { cartItems: 3 });
|
|
|
48
72
|
custom delivery primitive.
|
|
49
73
|
- Read `process.env.NODE_ENV`, `import.meta.env`, `location`, or
|
|
50
74
|
`document.cookie` — pass `environment` explicitly.
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
75
|
+
- Touch globals from the **core** or from `createLogger()` — the
|
|
76
|
+
core installs no global listeners and reads no ambient state. The
|
|
77
|
+
**one opt-in exception** is host-owned: a host may install a
|
|
78
|
+
single global **error** capturer via the
|
|
79
|
+
[`./capture` subpath](#catch-uncaught-errors--capture-subpath) —
|
|
80
|
+
explicit, opt-in, routed through the same secure pipeline;
|
|
81
|
+
federated modules never install it. Web Vitals, view tracking,
|
|
82
|
+
network instrumentation, and a server/monitoring backend remain out
|
|
83
|
+
of scope — SafeSignal is an error-logging library, not a RUM product.
|
|
54
84
|
- Persist events to IndexedDB or any storage layer.
|
|
55
85
|
- Batch, sample, or deduplicate events by default (opt-in
|
|
56
86
|
batching is available via the `./transport-beacon` subpath).
|
|
@@ -194,6 +224,316 @@ getRootLogger().info('payment.authorized', { amount: 4200 });
|
|
|
194
224
|
context-merge precedence (root → logger chain → `correlation()`); host and
|
|
195
225
|
federated modules each contribute without per-`Logger` cost.
|
|
196
226
|
|
|
227
|
+
### Tag the delivery request with `traceparent`
|
|
228
|
+
|
|
229
|
+
Beyond the per-`LogRecord` trace fields above, the `./transport-otlp` transport
|
|
230
|
+
can also set a W3C `traceparent` (and `tracestate`) **request header** on the
|
|
231
|
+
delivery request itself, so a backend or collector can join the ingest request
|
|
232
|
+
to its trace. It is **off by default** — opt in per transport:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const transport = createOtlpTransport({
|
|
236
|
+
endpoint: 'https://otlp.example.com/v1/logs',
|
|
237
|
+
headers: { authorization: `Bearer ${token}` }, // sent only on the wire
|
|
238
|
+
injectTraceparent: true, // ← opt in
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
A delivery request carries the header **only when every event in the flushed
|
|
243
|
+
batch shares one valid trace context** (the common case for a burst of logs in
|
|
244
|
+
one span); a mixed-trace, trace-less, or empty batch sends no header — never an
|
|
245
|
+
arbitrary "representative" one. `tracestate` rides along only when it is
|
|
246
|
+
identical across the batch (and within the 512-char bound).
|
|
247
|
+
|
|
248
|
+
- **Carry-only / fail-safe**: built from the events' existing `context.trace`;
|
|
249
|
+
no ids are minted, header construction never throws into a logging call or
|
|
250
|
+
blocks delivery, and the event payload is byte-identical either way.
|
|
251
|
+
- **Secure**: the header carries only trace identifiers + bounded `tracestate`;
|
|
252
|
+
it never overwrites, duplicates, or exposes your auth/secret `headers`
|
|
253
|
+
(a consumer-supplied `traceparent` wins). Only `./transport-otlp` supports it
|
|
254
|
+
— `navigator.sendBeacon` cannot set custom request headers, so
|
|
255
|
+
`./transport-beacon` is out of scope.
|
|
256
|
+
|
|
257
|
+
## Catch uncaught errors — `./capture` subpath
|
|
258
|
+
|
|
259
|
+
Uncaught exceptions and unhandled promise rejections normally vanish —
|
|
260
|
+
they never reach your configured transports. The **opt-in** `./capture`
|
|
261
|
+
subpath lets a **host** route them through the same secure pipeline as
|
|
262
|
+
every other log:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { configureLogging, getRootLogger } from '@tallyrow/safesignal';
|
|
266
|
+
import { createBeaconTransport } from '@tallyrow/safesignal/transport-beacon';
|
|
267
|
+
import { installGlobalErrorCapture } from '@tallyrow/safesignal/capture';
|
|
268
|
+
|
|
269
|
+
configureLogging({
|
|
270
|
+
application: { name: 'checkout-web', version: '4.2.0' },
|
|
271
|
+
environment: 'production',
|
|
272
|
+
transports: [createBeaconTransport({ endpoint: 'https://logs.example.com/ingest' })],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Host installs once — returns a disposer.
|
|
276
|
+
const dispose = installGlobalErrorCapture(getRootLogger());
|
|
277
|
+
// …on teardown: dispose();
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
It emits an `error`-level event (`'Uncaught exception'` /
|
|
281
|
+
`'Unhandled promise rejection'`) carrying the serialized error and a
|
|
282
|
+
`safesignal.source` / `safesignal.errorType` marker, redacted +
|
|
283
|
+
sanitized like any log.
|
|
284
|
+
|
|
285
|
+
- **Host-owned, opt-in** (Principle VIII): it is **never** a side effect
|
|
286
|
+
of `createLogger()`; a **federated module never installs it** — only the
|
|
287
|
+
host that owns the runtime does. Pass it the host's `Logger`
|
|
288
|
+
(`getRootLogger()` or `createLogger({ module })`).
|
|
289
|
+
- **Fail-safe**: it never throws into the page and never breaks
|
|
290
|
+
rendering/navigation; a failing transport is swallowed to
|
|
291
|
+
`onInternalError`.
|
|
292
|
+
- **Additive**: it chains via `addEventListener` — your existing
|
|
293
|
+
`window.onerror`/handlers keep firing; it never `preventDefault()`s.
|
|
294
|
+
- **Errors only** — no view tracking, web vitals, or network
|
|
295
|
+
instrumentation (that is RUM — out of scope). Duplicate package copies are
|
|
296
|
+
**isolated** (each capturer uses the `Logger` from its own copy).
|
|
297
|
+
|
|
298
|
+
## Catch React errors — `./framework-react` subpath
|
|
299
|
+
|
|
300
|
+
When a React component throws during render, the default outcome is a blank
|
|
301
|
+
screen and an error that never reaches your transports. The **opt-in**
|
|
302
|
+
`./framework-react` subpath is the **no-globals, per-component** counterpart to
|
|
303
|
+
`./capture`: a `<LogErrorBoundary>` and a `useLogError()` hook that route React
|
|
304
|
+
errors through your existing `Logger` and render a graceful fallback. `react` is
|
|
305
|
+
a **peer dependency** (`>=16.8`) — the core and every other subpath stay
|
|
306
|
+
React-free.
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { configureLogging, createLogger } from '@tallyrow/safesignal';
|
|
310
|
+
import { LoggerProvider, LogErrorBoundary, useLogError }
|
|
311
|
+
from '@tallyrow/safesignal/framework-react';
|
|
312
|
+
|
|
313
|
+
configureLogging({ application: { name: 'checkout-web' }, environment: 'production', transports: [/* … */] });
|
|
314
|
+
const log = createLogger({ module: 'checkout' });
|
|
315
|
+
|
|
316
|
+
function App() {
|
|
317
|
+
return (
|
|
318
|
+
<LoggerProvider logger={log}>
|
|
319
|
+
<LogErrorBoundary fallback={<p>Something went wrong.</p>}>
|
|
320
|
+
<Checkout />
|
|
321
|
+
</LogErrorBoundary>
|
|
322
|
+
</LoggerProvider>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
A boundary-caught error emits an `error`-level event (`'React render error'`)
|
|
328
|
+
carrying the serialized error and the React **component stack**
|
|
329
|
+
(`safesignal.react.componentStack`), with `safesignal.source:
|
|
330
|
+
'react-error-boundary'`, redacted + sanitized like any log. For the errors a
|
|
331
|
+
boundary **cannot** catch — event handlers, async/`Promise` callbacks, effects —
|
|
332
|
+
use the hook:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
function SaveButton() {
|
|
336
|
+
const logError = useLogError(); // stable callback; resolves the logger from context
|
|
337
|
+
const onClick = async () => {
|
|
338
|
+
try { await save(); } catch (err) { logError(err, { 'safesignal.action': 'save' }); }
|
|
339
|
+
};
|
|
340
|
+
return <button onClick={onClick}>Save</button>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- **No globals** (Principle VIII): patches nothing, attaches no `window`
|
|
345
|
+
listeners — the explicit contrast with `./capture`'s host-level install. The
|
|
346
|
+
two are complementary and can be used together.
|
|
347
|
+
- **Fail-safe** (Principle III): a logging (or `onError`) failure is swallowed
|
|
348
|
+
and the fallback still renders; React semantics keep it loop-free.
|
|
349
|
+
- **Fail-closed** (Principle V): errors route through the same redaction
|
|
350
|
+
pipeline as any log; if redaction fails the event is dropped.
|
|
351
|
+
- **Explicit logger**: provide it via `<LoggerProvider>` or a `logger` prop /
|
|
352
|
+
`useLogError(logger)` argument. With no logger resolvable, the helpers are a
|
|
353
|
+
**safe no-op** (never throw). `<LogErrorBoundary>` also accepts `onError`,
|
|
354
|
+
`resetKeys`, and a render-prop `fallback={(error, reset) => …}` for recovery.
|
|
355
|
+
Duplicate package copies are **isolated** (each routes through the logger it is
|
|
356
|
+
handed).
|
|
357
|
+
|
|
358
|
+
## Catch Vue errors — `./framework-vue` subpath
|
|
359
|
+
|
|
360
|
+
The Vue 3 counterpart of `./framework-react`. When a Vue component throws during
|
|
361
|
+
render, a lifecycle hook, a watcher, or a template handler, that error never
|
|
362
|
+
reaches your transports. The **opt-in** `./framework-vue` subpath is the
|
|
363
|
+
**no-globals, per-app** counterpart to `./capture`: an `app.config.errorHandler`
|
|
364
|
+
adapter plus the `useLogError()` and `useErrorCapture()` composables that route
|
|
365
|
+
Vue errors through your existing `Logger`. `vue` is a **peer dependency**
|
|
366
|
+
(`>=3.0`) — the core and every other subpath stay Vue-free.
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createApp } from 'vue';
|
|
370
|
+
import { createLogger } from '@tallyrow/safesignal';
|
|
371
|
+
import { safesignalErrorHandler } from '@tallyrow/safesignal/framework-vue';
|
|
372
|
+
|
|
373
|
+
const log = createLogger({ module: 'checkout' });
|
|
374
|
+
|
|
375
|
+
createApp(App)
|
|
376
|
+
.use(safesignalErrorHandler, { logger: log }) // sets app.config.errorHandler + provides the logger
|
|
377
|
+
.mount('#app');
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
A framework error emits an `error`-level event (`'Vue error'`) carrying the
|
|
381
|
+
serialized error and best-effort Vue context (`safesignal.vue.info`,
|
|
382
|
+
`safesignal.vue.componentName`), with `safesignal.source: 'vue-error-handler'`,
|
|
383
|
+
redacted + sanitized like any log. Prefer to wire it yourself? The factory is
|
|
384
|
+
side-effect-free: `app.config.errorHandler = createErrorHandler(log)`.
|
|
385
|
+
|
|
386
|
+
For errors Vue's handler can't catch (async/`try`-`catch`, native listeners), use
|
|
387
|
+
`useLogError()`; to contain and recover a subtree, use `useErrorCapture()`:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import { ref } from 'vue';
|
|
391
|
+
import { useLogError, useErrorCapture } from '@tallyrow/safesignal/framework-vue';
|
|
392
|
+
|
|
393
|
+
// In a component's setup():
|
|
394
|
+
const logError = useLogError(); // stable callback; resolves the provided logger
|
|
395
|
+
async function onClick() {
|
|
396
|
+
try { await save(); } catch (err) { logError(err, { 'safesignal.action': 'save' }); }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// In a wrapper component's setup() — a subtree boundary:
|
|
400
|
+
const failed = ref(false);
|
|
401
|
+
useErrorCapture({ onError: () => { failed.value = true; } });
|
|
402
|
+
// descendant errors log once (safesignal.source: 'vue-error-captured') and, by
|
|
403
|
+
// default, do NOT also reach the app-level handler. Pass { propagate: true } to bubble.
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
- **No globals** (Principle VIII): patches nothing, attaches no `window`
|
|
407
|
+
listeners — the explicit contrast with `./capture`. Complementary; usable together.
|
|
408
|
+
- **Fail-safe** (Principle III): a logging (or `onError`) failure is swallowed and
|
|
409
|
+
the app keeps running; no error loop.
|
|
410
|
+
- **Fail-closed** (Principle V): errors route through the same redaction pipeline
|
|
411
|
+
as any log; if redaction fails the event is dropped.
|
|
412
|
+
- **Explicit logger**: provide it via the plugin (or `useLogError(logger)` /
|
|
413
|
+
`useErrorCapture({ logger })`). With no logger resolvable, the helpers are a
|
|
414
|
+
**safe no-op** (never throw). Duplicate package copies are **isolated**.
|
|
415
|
+
|
|
416
|
+
## Readable error stacks — `./stacks` subpath
|
|
417
|
+
|
|
418
|
+
A raw browser error stack is a wall of minified, framework-internal noise. The
|
|
419
|
+
**opt-in** `./stacks` subpath parses an error's stack into **trimmed, structured
|
|
420
|
+
frames** (function / file / line / column), and — when you supply a **synchronous
|
|
421
|
+
source-map resolver** — maps minified production frames back to original source
|
|
422
|
+
positions. **Off by default.**
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import { configureLogging, getRootLogger } from '@tallyrow/safesignal';
|
|
426
|
+
import { createStackNormalizer } from '@tallyrow/safesignal/stacks';
|
|
427
|
+
|
|
428
|
+
// Optional: a SYNCHRONOUS resolver over source maps you have already loaded.
|
|
429
|
+
const resolver = (f) => mySourceMaps.lookup(f.file, f.line, f.column) ?? null;
|
|
430
|
+
|
|
431
|
+
configureLogging({
|
|
432
|
+
application: { name: 'checkout-web', version: '4.2.0' },
|
|
433
|
+
environment: 'production',
|
|
434
|
+
transports: [/* … */],
|
|
435
|
+
normalizeStack: createStackNormalizer({ resolver, maxFrames: 30 }), // OFF unless set
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
getRootLogger().error('checkout failed', { orderId: 'ord_9f3' }, new Error('boom'));
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The delivered **error** event gains `attributes['safesignal.stack']` — an ordered
|
|
442
|
+
array of `{ function?, file?, line?, column?, original? }` (the raw `error.stack`
|
|
443
|
+
string is preserved unchanged). Other events are untouched.
|
|
444
|
+
|
|
445
|
+
- **Trimmed**: `node_modules`, engine-internal, and boilerplate frames are removed
|
|
446
|
+
by default (`includeNodeModules` / `includeInternal` opt back in); bounded to
|
|
447
|
+
`maxFrames` (default 30, max 100).
|
|
448
|
+
- **Source-mapped**: with a **synchronous** `resolver`, resolvable frames carry
|
|
449
|
+
`original`; an unmappable frame is left as-is. SafeSignal does **no** async work
|
|
450
|
+
or `.map` fetching — you load your maps; SafeSignal calls a fast sync lookup.
|
|
451
|
+
- **Safe**: frames ride in `attributes`, so a secret in a frame URL's query is
|
|
452
|
+
scrubbed by the pipeline (whole-value guarantee). Off by default, fail-safe
|
|
453
|
+
(a throwing parser/resolver never breaks the page — the error is always
|
|
454
|
+
delivered), runtime-level (no per-`Logger` cost), and **no new dependency**.
|
|
455
|
+
|
|
456
|
+
## Error breadcrumbs — recent-event context on errors
|
|
457
|
+
|
|
458
|
+
When an error is logged, the hardest debugging question is "what happened *just
|
|
459
|
+
before* this?" Enable **opt-in error breadcrumbs** and every error log
|
|
460
|
+
automatically carries a bounded trail of the most recent events plus the error's
|
|
461
|
+
cause chain — built only from SafeSignal's own already sanitized + redacted
|
|
462
|
+
events. **Off by default.**
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
import { configureLogging, getRootLogger } from '@tallyrow/safesignal';
|
|
466
|
+
|
|
467
|
+
configureLogging({
|
|
468
|
+
application: { name: 'checkout-web', version: '4.2.0' },
|
|
469
|
+
environment: 'production',
|
|
470
|
+
transports: [/* … */],
|
|
471
|
+
breadcrumbs: true, // or { maxEvents: 30 } — default 20, max 100
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const log = getRootLogger();
|
|
475
|
+
log.info('checkout opened', { cartItems: 3 });
|
|
476
|
+
log.warn('coupon expired');
|
|
477
|
+
log.error('checkout failed', { orderId: 'ord_9f3' },
|
|
478
|
+
new Error('checkout failed', { cause: new Error('payment processor 5xx') }));
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The delivered **error** event gains two documented, machine-parseable attribute
|
|
482
|
+
fields (other events are untouched):
|
|
483
|
+
|
|
484
|
+
- `attributes['safesignal.breadcrumbs']` — the recent events, oldest→newest, each
|
|
485
|
+
`{ ts, level, message, app?, module?, attributes? }` (host vs. federated-module
|
|
486
|
+
origin stays distinguishable).
|
|
487
|
+
- `attributes['safesignal.errorCauses']` — the error's nested cause chain,
|
|
488
|
+
outermost→root, each `{ name, message }`.
|
|
489
|
+
|
|
490
|
+
- **Bounded & cheap**: a single runtime-level ring buffer — constant memory (≤
|
|
491
|
+
`maxEvents`), constant-cost recording, **no** per-`Logger` cost. Duplicate
|
|
492
|
+
package copies are **isolated** (each runtime owns its buffer).
|
|
493
|
+
- **Safe**: breadcrumbs carry only the post-redaction event; the cause chain runs
|
|
494
|
+
through the same redaction. It never mutates other (or already-delivered)
|
|
495
|
+
events, and never throws into the page — an error is always delivered, with or
|
|
496
|
+
without the trail.
|
|
497
|
+
|
|
498
|
+
## Pretty dev logs — `./dev-console` subpath
|
|
499
|
+
|
|
500
|
+
The built-in `ConsoleTransport` hands devtools the message plus the structured
|
|
501
|
+
event object — correct and safe, but a wall of JSON to scan locally. The
|
|
502
|
+
**opt-in** `./dev-console` subpath ships `DevConsoleTransport`: a pretty,
|
|
503
|
+
**development-only** sibling that renders the *same* already sanitized + redacted
|
|
504
|
+
event as a collapsed, level-styled group (icon/color, message, `app · module ·
|
|
505
|
+
env`, attributes, error, and a trace link). Select it **only** in development so
|
|
506
|
+
your bundler tree-shakes it out of production entirely:
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import { configureLogging, getRootLogger, ConsoleTransport } from '@tallyrow/safesignal';
|
|
510
|
+
import { DevConsoleTransport } from '@tallyrow/safesignal/dev-console';
|
|
511
|
+
|
|
512
|
+
configureLogging({
|
|
513
|
+
application: { name: 'checkout-web', version: '4.2.0' },
|
|
514
|
+
environment: import.meta.env.DEV ? 'development' : 'production',
|
|
515
|
+
transports: [
|
|
516
|
+
import.meta.env.DEV
|
|
517
|
+
? DevConsoleTransport({ traceUrl: ({ traceId }) => `https://trace.example/${traceId}` })
|
|
518
|
+
: ConsoleTransport(),
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
getRootLogger().info('checkout opened', { cartItems: 3 });
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
- **Genuine zero production cost**: the dev branch is dead-code-eliminated from
|
|
526
|
+
your production build, so the renderer ships **0 bytes** there. SafeSignal's
|
|
527
|
+
default `.` entry (and `ConsoleTransport`) is byte-unchanged.
|
|
528
|
+
- **Runtime-gated + defensive**: it renders pretty only when the event's
|
|
529
|
+
`environment === 'development'`; in any other environment — or where rich
|
|
530
|
+
console features are absent — it behaves exactly like `ConsoleTransport`
|
|
531
|
+
(`console[level](message, event)`), even if mistakenly used in production.
|
|
532
|
+
- **Structured-only & safe**: it renders **only** the post-pipeline event (no
|
|
533
|
+
re-serialization of app objects), attaches **no** globals, reads no ambient
|
|
534
|
+
state, and never throws into the page. The optional `traceUrl` formatter is
|
|
535
|
+
carry-only — built from the event's existing trace ids, no ids minted.
|
|
536
|
+
|
|
197
537
|
## Level configuration
|
|
198
538
|
|
|
199
539
|
In `production`, `debug` and `info` are dropped by default. Raise the
|
|
@@ -252,7 +592,7 @@ log.info('order placed', { orderId: order.id, total: order.total });
|
|
|
252
592
|
enumeration of DO / DON'T patterns, the sanitizer's bounded-input
|
|
253
593
|
rules, the redactor's default denylist and shape rules,
|
|
254
594
|
`createRedactor()` composition, `scrubUrl()` usage, the
|
|
255
|
-
diagnostics contract, and — per constitution Principle
|
|
595
|
+
diagnostics contract, and — per constitution Principle VII — every
|
|
256
596
|
documented behavior that drops, transforms, or otherwise bounds an
|
|
257
597
|
event before delivery (level-filter drops, fail-closed redactor
|
|
258
598
|
drops, sanitizer truncation markers, URL-scrubber replacements,
|
|
@@ -352,11 +692,12 @@ package copies", and "Vendor neutrality".
|
|
|
352
692
|
|
|
353
693
|
## Project resources
|
|
354
694
|
|
|
355
|
-
[](https://github.com/TallyRow/safesignal/actions/workflows/ci.yml)
|
|
696
|
+
[](https://www.npmjs.com/package/@tallyrow/safesignal)
|
|
356
697
|
|
|
357
698
|
Community and legal:
|
|
358
699
|
|
|
359
|
-
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to file issues,
|
|
700
|
+
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to file issues, open PRs, sign commits (DCO)
|
|
360
701
|
- [`SECURITY.md`](SECURITY.md) — vulnerability disclosure policy
|
|
361
702
|
- [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) — Contributor Covenant 2.1
|
|
362
703
|
- [`GOVERNANCE.md`](GOVERNANCE.md) — how project decisions get made
|
|
@@ -381,21 +722,17 @@ Reference docs and design history:
|
|
|
381
722
|
|
|
382
723
|
The following are forward-looking items (not shipping today):
|
|
383
724
|
|
|
384
|
-
- **Trace-context propagation** — W3C Trace Context (`traceparent`,
|
|
385
|
-
`tracestate`) for correlating frontend logs with backend traces.
|
|
386
725
|
- **OTLP/HTTP+protobuf encoding** for the
|
|
387
726
|
[`./transport-otlp`](#ship-logs-to-otlp--transport-otlp-subpath)
|
|
388
727
|
subpath — the subpath ships **JSON** today behind an internal
|
|
389
728
|
encoding seam; a protobuf encoder is an additive follow-up (no
|
|
390
729
|
public-API change).
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
OTLP-formatted events. SafeSignal stays a small vendor-neutral
|
|
398
|
-
SDK; the server lives in its own repo when it ships.
|
|
730
|
+
|
|
731
|
+
SafeSignal is singly focused: it captures your errors — explicit
|
|
732
|
+
`log.error`, framework boundaries, and opt-in global capture — and
|
|
733
|
+
ships them securely to any backend. It is **not** a RUM/monitoring
|
|
734
|
+
product (no Web Vitals, view tracking, network instrumentation, or
|
|
735
|
+
server backend) and is not planned to become one.
|
|
399
736
|
|
|
400
737
|
## Migration history
|
|
401
738
|
|
package/dist/capture.cjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/capture/index.ts
|
|
4
|
+
var SOURCE = "global-error-capture";
|
|
5
|
+
function noop() {
|
|
6
|
+
}
|
|
7
|
+
function safeNotify(hook, cause) {
|
|
8
|
+
if (!hook) return;
|
|
9
|
+
try {
|
|
10
|
+
hook(cause instanceof Error ? cause : new Error(String(cause)));
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function installGlobalErrorCapture(logger, options = {}) {
|
|
15
|
+
const target = options.target ?? globalThis;
|
|
16
|
+
const canListen = typeof target.addEventListener === "function" && typeof target.removeEventListener === "function";
|
|
17
|
+
if (!canListen) return noop;
|
|
18
|
+
let disposed = false;
|
|
19
|
+
let inFlight = false;
|
|
20
|
+
const emit = (message, errorType, errorValue, extra) => {
|
|
21
|
+
if (inFlight) return;
|
|
22
|
+
inFlight = true;
|
|
23
|
+
try {
|
|
24
|
+
const attributes = {
|
|
25
|
+
"safesignal.source": SOURCE,
|
|
26
|
+
"safesignal.errorType": errorType,
|
|
27
|
+
...extra ?? {}
|
|
28
|
+
};
|
|
29
|
+
logger.error(message, attributes, errorValue);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
safeNotify(options.onInternalError, err);
|
|
32
|
+
} finally {
|
|
33
|
+
inFlight = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const handleError = (event) => {
|
|
37
|
+
try {
|
|
38
|
+
const e = event;
|
|
39
|
+
const hasError = e.error !== void 0 && e.error !== null;
|
|
40
|
+
const extra = {};
|
|
41
|
+
if (!hasError) {
|
|
42
|
+
if (typeof e.filename === "string") extra.filename = e.filename;
|
|
43
|
+
if (typeof e.lineno === "number") extra.lineno = e.lineno;
|
|
44
|
+
if (typeof e.colno === "number") extra.colno = e.colno;
|
|
45
|
+
}
|
|
46
|
+
const errorValue = hasError ? e.error : typeof e.message === "string" ? e.message : "Uncaught exception";
|
|
47
|
+
emit(
|
|
48
|
+
"Uncaught exception",
|
|
49
|
+
"uncaught-exception",
|
|
50
|
+
errorValue,
|
|
51
|
+
Object.keys(extra).length > 0 ? extra : void 0
|
|
52
|
+
);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
safeNotify(options.onInternalError, err);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const handleRejection = (event) => {
|
|
58
|
+
try {
|
|
59
|
+
const e = event;
|
|
60
|
+
emit("Unhandled promise rejection", "unhandled-rejection", e.reason);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
safeNotify(options.onInternalError, err);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
target.addEventListener("error", handleError);
|
|
66
|
+
target.addEventListener("unhandledrejection", handleRejection);
|
|
67
|
+
return () => {
|
|
68
|
+
if (disposed) return;
|
|
69
|
+
disposed = true;
|
|
70
|
+
target.removeEventListener("error", handleError);
|
|
71
|
+
target.removeEventListener("unhandledrejection", handleRejection);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
exports.installGlobalErrorCapture = installGlobalErrorCapture;
|
|
76
|
+
//# sourceMappingURL=capture.cjs.map
|
|
77
|
+
//# sourceMappingURL=capture.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/capture/index.ts"],"names":[],"mappings":"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","file":"capture.cjs","sourcesContent":["/**\n * Opt-in global error capture — the `./capture` subpath.\n *\n * A **host-level**, opt-in install that routes **uncaught exceptions** and\n * **unhandled promise rejections** through the host `Logger`'s existing secure\n * pipeline (sanitize → URL-scrub → redact → guard → transport). It is the single\n * explicit host-installed runtime-level global handler Principle VIII v1.5.0\n * sanctions — never a side effect of `createLogger()`, never installed by a\n * federated module.\n *\n * Properties (see `specs/013-global-error-capture/contracts/capture-api.md`):\n * - Fail-closed: emits via `logger.error`, so stacks/messages are redacted +\n * sanitized (drop-on-failure) before any transport sees them.\n * - Fail-safe: never throws/rejects into the page; internal failures are\n * swallowed (routed to `options.onInternalError`).\n * - Additive: attaches via `addEventListener` — never assigns `window.onerror`\n * and never calls `preventDefault()`, so existing handlers keep firing.\n * - Loop-safe: a re-entrancy guard stops an error raised during emit from\n * re-capturing.\n * - Errors only: attaches ONLY `error` + `unhandledrejection` (not RUM).\n *\n * The only `src/` import is **type-only** from `../api/types.js`, so this bundle\n * shares no runtime state with the core (it operates through the passed\n * `Logger`, which closes over the host's configured runtime).\n */\n\nimport type { Attributes, Logger } from '../api/types.js';\n\n/** Options for {@link installGlobalErrorCapture}. */\nexport interface GlobalErrorCaptureOptions {\n /** Where to attach listeners. Default: `globalThis`. */\n target?: EventTarget;\n /**\n * Diagnostics hook for the capturer's OWN failures (event-build/dispatch\n * throw). Invoked fail-safe — its own throw is swallowed. Distinct from the\n * runtime's `onInternalError`.\n */\n onInternalError?: (err: Error) => void;\n}\n\n/** Removes the installed listeners and stops capture. Idempotent. */\nexport type GlobalErrorCaptureDisposer = () => void;\n\nconst SOURCE = 'global-error-capture';\n\nfunction noop(): void {\n /* no-op disposer */\n}\n\nfunction safeNotify(\n hook: ((err: Error) => void) | undefined,\n cause: unknown,\n): void {\n if (!hook) return;\n try {\n hook(cause instanceof Error ? cause : new Error(String(cause)));\n } catch {\n // A diagnostics hook that itself throws must not break capture.\n }\n}\n\n/**\n * Install host-level capture of uncaught exceptions + unhandled promise\n * rejections, routed through `logger`'s configured pipeline. Returns a disposer.\n * Opt-in, host-owned; never a side effect of `createLogger()`. Never throws.\n */\nexport function installGlobalErrorCapture(\n logger: Logger,\n options: GlobalErrorCaptureOptions = {},\n): GlobalErrorCaptureDisposer {\n const target: EventTarget =\n options.target ?? (globalThis as unknown as EventTarget);\n\n // Safe no-op where the target cannot register listeners (SSR / worker).\n const canListen =\n typeof (target as { addEventListener?: unknown }).addEventListener ===\n 'function' &&\n typeof (target as { removeEventListener?: unknown }).removeEventListener ===\n 'function';\n if (!canListen) return noop;\n\n let disposed = false;\n let inFlight = false;\n\n const emit = (\n message: string,\n errorType: 'uncaught-exception' | 'unhandled-rejection',\n errorValue: unknown,\n extra?: Attributes,\n ): void => {\n if (inFlight) return; // loop-safety: drop a capture raised during emit.\n inFlight = true;\n try {\n const attributes: Attributes = {\n 'safesignal.source': SOURCE,\n 'safesignal.errorType': errorType,\n ...(extra ?? {}),\n };\n logger.error(message, attributes, errorValue);\n } catch (err) {\n safeNotify(options.onInternalError, err);\n } finally {\n inFlight = false;\n }\n };\n\n const handleError = (event: Event): void => {\n try {\n const e = event as ErrorEvent;\n const hasError = e.error !== undefined && e.error !== null;\n const extra: Attributes = {};\n if (!hasError) {\n // Cross-origin \"Script error.\" cases carry no error object.\n if (typeof e.filename === 'string') extra.filename = e.filename;\n if (typeof e.lineno === 'number') extra.lineno = e.lineno;\n if (typeof e.colno === 'number') extra.colno = e.colno;\n }\n const errorValue = hasError\n ? e.error\n : typeof e.message === 'string'\n ? e.message\n : 'Uncaught exception';\n emit(\n 'Uncaught exception',\n 'uncaught-exception',\n errorValue,\n Object.keys(extra).length > 0 ? extra : undefined,\n );\n } catch (err) {\n safeNotify(options.onInternalError, err);\n }\n };\n\n const handleRejection = (event: Event): void => {\n try {\n const e = event as PromiseRejectionEvent;\n emit('Unhandled promise rejection', 'unhandled-rejection', e.reason);\n } catch (err) {\n safeNotify(options.onInternalError, err);\n }\n };\n\n target.addEventListener('error', handleError);\n target.addEventListener('unhandledrejection', handleRejection);\n\n return (): void => {\n if (disposed) return;\n disposed = true;\n target.removeEventListener('error', handleError);\n target.removeEventListener('unhandledrejection', handleRejection);\n };\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { f as Logger } from './types-CZtSjgq5.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in global error capture — the `./capture` subpath.
|
|
5
|
+
*
|
|
6
|
+
* A **host-level**, opt-in install that routes **uncaught exceptions** and
|
|
7
|
+
* **unhandled promise rejections** through the host `Logger`'s existing secure
|
|
8
|
+
* pipeline (sanitize → URL-scrub → redact → guard → transport). It is the single
|
|
9
|
+
* explicit host-installed runtime-level global handler Principle VIII v1.5.0
|
|
10
|
+
* sanctions — never a side effect of `createLogger()`, never installed by a
|
|
11
|
+
* federated module.
|
|
12
|
+
*
|
|
13
|
+
* Properties (see `specs/013-global-error-capture/contracts/capture-api.md`):
|
|
14
|
+
* - Fail-closed: emits via `logger.error`, so stacks/messages are redacted +
|
|
15
|
+
* sanitized (drop-on-failure) before any transport sees them.
|
|
16
|
+
* - Fail-safe: never throws/rejects into the page; internal failures are
|
|
17
|
+
* swallowed (routed to `options.onInternalError`).
|
|
18
|
+
* - Additive: attaches via `addEventListener` — never assigns `window.onerror`
|
|
19
|
+
* and never calls `preventDefault()`, so existing handlers keep firing.
|
|
20
|
+
* - Loop-safe: a re-entrancy guard stops an error raised during emit from
|
|
21
|
+
* re-capturing.
|
|
22
|
+
* - Errors only: attaches ONLY `error` + `unhandledrejection` (not RUM).
|
|
23
|
+
*
|
|
24
|
+
* The only `src/` import is **type-only** from `../api/types.js`, so this bundle
|
|
25
|
+
* shares no runtime state with the core (it operates through the passed
|
|
26
|
+
* `Logger`, which closes over the host's configured runtime).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Options for {@link installGlobalErrorCapture}. */
|
|
30
|
+
interface GlobalErrorCaptureOptions {
|
|
31
|
+
/** Where to attach listeners. Default: `globalThis`. */
|
|
32
|
+
target?: EventTarget;
|
|
33
|
+
/**
|
|
34
|
+
* Diagnostics hook for the capturer's OWN failures (event-build/dispatch
|
|
35
|
+
* throw). Invoked fail-safe — its own throw is swallowed. Distinct from the
|
|
36
|
+
* runtime's `onInternalError`.
|
|
37
|
+
*/
|
|
38
|
+
onInternalError?: (err: Error) => void;
|
|
39
|
+
}
|
|
40
|
+
/** Removes the installed listeners and stops capture. Idempotent. */
|
|
41
|
+
type GlobalErrorCaptureDisposer = () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Install host-level capture of uncaught exceptions + unhandled promise
|
|
44
|
+
* rejections, routed through `logger`'s configured pipeline. Returns a disposer.
|
|
45
|
+
* Opt-in, host-owned; never a side effect of `createLogger()`. Never throws.
|
|
46
|
+
*/
|
|
47
|
+
declare function installGlobalErrorCapture(logger: Logger, options?: GlobalErrorCaptureOptions): GlobalErrorCaptureDisposer;
|
|
48
|
+
|
|
49
|
+
export { type GlobalErrorCaptureDisposer, type GlobalErrorCaptureOptions, installGlobalErrorCapture };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { f as Logger } from './types-CZtSjgq5.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in global error capture — the `./capture` subpath.
|
|
5
|
+
*
|
|
6
|
+
* A **host-level**, opt-in install that routes **uncaught exceptions** and
|
|
7
|
+
* **unhandled promise rejections** through the host `Logger`'s existing secure
|
|
8
|
+
* pipeline (sanitize → URL-scrub → redact → guard → transport). It is the single
|
|
9
|
+
* explicit host-installed runtime-level global handler Principle VIII v1.5.0
|
|
10
|
+
* sanctions — never a side effect of `createLogger()`, never installed by a
|
|
11
|
+
* federated module.
|
|
12
|
+
*
|
|
13
|
+
* Properties (see `specs/013-global-error-capture/contracts/capture-api.md`):
|
|
14
|
+
* - Fail-closed: emits via `logger.error`, so stacks/messages are redacted +
|
|
15
|
+
* sanitized (drop-on-failure) before any transport sees them.
|
|
16
|
+
* - Fail-safe: never throws/rejects into the page; internal failures are
|
|
17
|
+
* swallowed (routed to `options.onInternalError`).
|
|
18
|
+
* - Additive: attaches via `addEventListener` — never assigns `window.onerror`
|
|
19
|
+
* and never calls `preventDefault()`, so existing handlers keep firing.
|
|
20
|
+
* - Loop-safe: a re-entrancy guard stops an error raised during emit from
|
|
21
|
+
* re-capturing.
|
|
22
|
+
* - Errors only: attaches ONLY `error` + `unhandledrejection` (not RUM).
|
|
23
|
+
*
|
|
24
|
+
* The only `src/` import is **type-only** from `../api/types.js`, so this bundle
|
|
25
|
+
* shares no runtime state with the core (it operates through the passed
|
|
26
|
+
* `Logger`, which closes over the host's configured runtime).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Options for {@link installGlobalErrorCapture}. */
|
|
30
|
+
interface GlobalErrorCaptureOptions {
|
|
31
|
+
/** Where to attach listeners. Default: `globalThis`. */
|
|
32
|
+
target?: EventTarget;
|
|
33
|
+
/**
|
|
34
|
+
* Diagnostics hook for the capturer's OWN failures (event-build/dispatch
|
|
35
|
+
* throw). Invoked fail-safe — its own throw is swallowed. Distinct from the
|
|
36
|
+
* runtime's `onInternalError`.
|
|
37
|
+
*/
|
|
38
|
+
onInternalError?: (err: Error) => void;
|
|
39
|
+
}
|
|
40
|
+
/** Removes the installed listeners and stops capture. Idempotent. */
|
|
41
|
+
type GlobalErrorCaptureDisposer = () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Install host-level capture of uncaught exceptions + unhandled promise
|
|
44
|
+
* rejections, routed through `logger`'s configured pipeline. Returns a disposer.
|
|
45
|
+
* Opt-in, host-owned; never a side effect of `createLogger()`. Never throws.
|
|
46
|
+
*/
|
|
47
|
+
declare function installGlobalErrorCapture(logger: Logger, options?: GlobalErrorCaptureOptions): GlobalErrorCaptureDisposer;
|
|
48
|
+
|
|
49
|
+
export { type GlobalErrorCaptureDisposer, type GlobalErrorCaptureOptions, installGlobalErrorCapture };
|