@tallyrow/safesignal 1.0.1 → 1.3.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 +127 -7
- package/dist/index.cjs +81 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.mjs +81 -13
- package/dist/index.mjs.map +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.mjs.map +1 -1
- package/dist/transport-beacon.cjs +65 -50
- 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 +65 -50
- package/dist/transport-beacon.mjs.map +1 -1
- package/dist/transport-otlp.cjs +621 -0
- package/dist/transport-otlp.cjs.map +1 -0
- package/dist/transport-otlp.d.cts +71 -0
- package/dist/transport-otlp.d.ts +71 -0
- package/dist/transport-otlp.mjs +619 -0
- package/dist/transport-otlp.mjs.map +1 -0
- package/dist/{types-D-xVvmvX.d.cts → types-BiRyHi1e.d.cts} +20 -1
- package/dist/{types-D-xVvmvX.d.ts → types-BiRyHi1e.d.ts} +20 -1
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -43,8 +43,9 @@ log.info('checkout opened', { cartItems: 3 });
|
|
|
43
43
|
|
|
44
44
|
- Ship an HTTP/beacon transport in the default entry — use the
|
|
45
45
|
`./transport-beacon` subpath for the first-party body-only HTTPS
|
|
46
|
-
transport,
|
|
47
|
-
|
|
46
|
+
transport, the `./transport-otlp` subpath to ship OTLP/HTTP+JSON
|
|
47
|
+
to any OTLP backend, or implement `Transport` yourself for a
|
|
48
|
+
custom delivery primitive.
|
|
48
49
|
- Read `process.env.NODE_ENV`, `import.meta.env`, `location`, or
|
|
49
50
|
`document.cookie` — pass `environment` explicitly.
|
|
50
51
|
- Install global listeners or singletons (RUM-style automatic
|
|
@@ -104,6 +105,125 @@ The transport:
|
|
|
104
105
|
`transport_shutdown_failed`. Wire the hook to **both** layers
|
|
105
106
|
above for full coverage.
|
|
106
107
|
|
|
108
|
+
## Ship logs to OTLP — `./transport-otlp` subpath
|
|
109
|
+
|
|
110
|
+
To deliver SafeSignal's events to **any OTLP-compatible backend**
|
|
111
|
+
(Datadog, Honeycomb, Grafana, an OpenTelemetry Collector,
|
|
112
|
+
ClickHouse, …), import `createOtlpTransport` from the
|
|
113
|
+
`./transport-otlp` subpath. It emits standard **OTLP/HTTP+JSON**
|
|
114
|
+
logs — vendor-neutral, with zero new runtime dependencies and no
|
|
115
|
+
`@opentelemetry/*` in the bundle.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { configureLogging, getRootLogger } from '@tallyrow/safesignal';
|
|
119
|
+
import { createOtlpTransport } from '@tallyrow/safesignal/transport-otlp';
|
|
120
|
+
|
|
121
|
+
configureLogging({
|
|
122
|
+
application: { name: 'checkout-web', version: '4.2.0' },
|
|
123
|
+
environment: 'production',
|
|
124
|
+
transports: [
|
|
125
|
+
createOtlpTransport({
|
|
126
|
+
endpoint: 'https://otlp.example.com/v1/logs', // full OTLP logs URL, HTTPS
|
|
127
|
+
headers: { 'x-api-key': process.env.OTLP_API_KEY! }, // sent only on the wire
|
|
128
|
+
batching: { maxBatchSize: 20, maxBatchAgeMs: 5000 },
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
getRootLogger().info('checkout.started', { cartId: 'c_123', itemCount: 3 });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
What it guarantees:
|
|
137
|
+
|
|
138
|
+
- **OTLP/HTTP+JSON** `LogRecord`s with your application/module/
|
|
139
|
+
environment identity mapped to the OTLP `Resource`
|
|
140
|
+
(`service.name`, `service.version`, `deployment.environment`;
|
|
141
|
+
`module.*` per-record). Levels map to OTLP severity
|
|
142
|
+
(`debug`→5, `info`→9, `warn`→13, `error`→17).
|
|
143
|
+
- **Fail-safe**: `fetch` with `keepalive` delivery, **no retry** —
|
|
144
|
+
a down/slow/erroring backend never throws into your code and
|
|
145
|
+
never breaks the page. Failed batches are dropped with one
|
|
146
|
+
rate-limited `onInternalError` notice per failure class
|
|
147
|
+
(`oversized_event`, `buffer_overflow`, `delivery_unavailable`,
|
|
148
|
+
`send_failed`, `partial_rejection`, `serialize_failed`,
|
|
149
|
+
`shutdown_failed`).
|
|
150
|
+
- **Secure**: events are already redacted before the transport
|
|
151
|
+
sees them; auth headers are sent only on the request and never
|
|
152
|
+
appear in payloads, diagnostics, or the bundle. HTTPS-only
|
|
153
|
+
(loopback `http://` requires explicit `allowInsecureLoopback`).
|
|
154
|
+
- **Lightweight & federated**: the transport is configured once at
|
|
155
|
+
the runtime level; the host owns it, federated modules do not
|
|
156
|
+
replace it, and duplicate package copies are **isolated**. Local
|
|
157
|
+
collectors: `endpoint: 'http://localhost:4318/v1/logs'` with
|
|
158
|
+
`allowInsecureLoopback: true`.
|
|
159
|
+
|
|
160
|
+
## Correlate logs with traces — W3C trace context
|
|
161
|
+
|
|
162
|
+
Supply a **W3C Trace Context** and SafeSignal carries `trace_id` / `span_id`
|
|
163
|
+
on every event; when shipped via `./transport-otlp`, they populate the OTLP
|
|
164
|
+
`LogRecord`'s standard `traceId` / `spanId` / `flags` fields, so any backend
|
|
165
|
+
joins each log to its trace. SafeSignal is **carry-only** — it never mints ids.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { configureLogging, getRootLogger, parseTraceparent } from '@tallyrow/safesignal';
|
|
169
|
+
|
|
170
|
+
// From a header string the app already holds (e.g. SSR-injected):
|
|
171
|
+
const trace = parseTraceparent('00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01');
|
|
172
|
+
|
|
173
|
+
configureLogging({
|
|
174
|
+
application: { name: 'checkout-web' },
|
|
175
|
+
environment: 'production',
|
|
176
|
+
context: trace ? { trace } : {},
|
|
177
|
+
// …or dynamically, per emit, from your tracer's active span:
|
|
178
|
+
correlation: () => {
|
|
179
|
+
const s = myTracer.activeSpan();
|
|
180
|
+
return s ? { trace: { traceId: s.traceId, spanId: s.spanId, traceFlags: 1 } } : {};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
getRootLogger().info('payment.authorized', { amount: 4200 });
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- **Carry-only / fail-safe**: no supplied context ⇒ no trace fields; a malformed
|
|
188
|
+
`traceparent`, wrong-length/all-zero id, or oversized `tracestate` is dropped
|
|
189
|
+
fail-closed — the event still ships, no throw.
|
|
190
|
+
- **Secure**: trace ids are identifiers, not secrets; `tracestate` is bounded;
|
|
191
|
+
existing redaction is unaffected.
|
|
192
|
+
- **Vendor-neutral**: pure W3C — works with any tracer; the `./transport-otlp`
|
|
193
|
+
bundle stays `@opentelemetry`-free. Trace context layers through the same
|
|
194
|
+
context-merge precedence (root → logger chain → `correlation()`); host and
|
|
195
|
+
federated modules each contribute without per-`Logger` cost.
|
|
196
|
+
|
|
197
|
+
### Tag the delivery request with `traceparent`
|
|
198
|
+
|
|
199
|
+
Beyond the per-`LogRecord` trace fields above, the `./transport-otlp` transport
|
|
200
|
+
can also set a W3C `traceparent` (and `tracestate`) **request header** on the
|
|
201
|
+
delivery request itself, so a backend or collector can join the ingest request
|
|
202
|
+
to its trace. It is **off by default** — opt in per transport:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const transport = createOtlpTransport({
|
|
206
|
+
endpoint: 'https://otlp.example.com/v1/logs',
|
|
207
|
+
headers: { authorization: `Bearer ${token}` }, // sent only on the wire
|
|
208
|
+
injectTraceparent: true, // ← opt in
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
A delivery request carries the header **only when every event in the flushed
|
|
213
|
+
batch shares one valid trace context** (the common case for a burst of logs in
|
|
214
|
+
one span); a mixed-trace, trace-less, or empty batch sends no header — never an
|
|
215
|
+
arbitrary "representative" one. `tracestate` rides along only when it is
|
|
216
|
+
identical across the batch (and within the 512-char bound).
|
|
217
|
+
|
|
218
|
+
- **Carry-only / fail-safe**: built from the events' existing `context.trace`;
|
|
219
|
+
no ids are minted, header construction never throws into a logging call or
|
|
220
|
+
blocks delivery, and the event payload is byte-identical either way.
|
|
221
|
+
- **Secure**: the header carries only trace identifiers + bounded `tracestate`;
|
|
222
|
+
it never overwrites, duplicates, or exposes your auth/secret `headers`
|
|
223
|
+
(a consumer-supplied `traceparent` wins). Only `./transport-otlp` supports it
|
|
224
|
+
— `navigator.sendBeacon` cannot set custom request headers, so
|
|
225
|
+
`./transport-beacon` is out of scope.
|
|
226
|
+
|
|
107
227
|
## Level configuration
|
|
108
228
|
|
|
109
229
|
In `production`, `debug` and `info` are dropped by default. Raise the
|
|
@@ -291,11 +411,11 @@ Reference docs and design history:
|
|
|
291
411
|
|
|
292
412
|
The following are forward-looking items (not shipping today):
|
|
293
413
|
|
|
294
|
-
- **
|
|
295
|
-
`
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
414
|
+
- **OTLP/HTTP+protobuf encoding** for the
|
|
415
|
+
[`./transport-otlp`](#ship-logs-to-otlp--transport-otlp-subpath)
|
|
416
|
+
subpath — the subpath ships **JSON** today behind an internal
|
|
417
|
+
encoding seam; a protobuf encoder is an additive follow-up (no
|
|
418
|
+
public-API change).
|
|
299
419
|
- **RUM features** — Web Vitals, automatic error capture, view
|
|
300
420
|
tracking, network instrumentation (planned as opt-in subpaths
|
|
301
421
|
under `./rum-*`).
|
package/dist/index.cjs
CHANGED
|
@@ -20,12 +20,16 @@ function mergeContexts(...sources) {
|
|
|
20
20
|
src.attributes
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
|
+
if (src.trace !== void 0) {
|
|
24
|
+
merged.trace = src.trace;
|
|
25
|
+
}
|
|
23
26
|
}
|
|
24
27
|
const out = {};
|
|
25
28
|
if (merged.application !== void 0) out.application = merged.application;
|
|
26
29
|
if (merged.module !== void 0) out.module = merged.module;
|
|
27
30
|
if (merged.environment !== void 0) out.environment = merged.environment;
|
|
28
31
|
if (merged.attributes !== void 0) out.attributes = merged.attributes;
|
|
32
|
+
if (merged.trace !== void 0) out.trace = merged.trace;
|
|
29
33
|
return out;
|
|
30
34
|
}
|
|
31
35
|
function deepMergeAttributes(earlier, later) {
|
|
@@ -94,7 +98,7 @@ function escapeControlChars(value) {
|
|
|
94
98
|
if (!HAS_CONTROL_CHAR.test(value)) return value;
|
|
95
99
|
return value.replace(CONTROL_CHAR_GLOBAL, (ch) => {
|
|
96
100
|
const code = ch.charCodeAt(0);
|
|
97
|
-
return
|
|
101
|
+
return `\\u${code.toString(16).padStart(4, "0")}`;
|
|
98
102
|
});
|
|
99
103
|
}
|
|
100
104
|
var controlCharGuard = (event, _config) => {
|
|
@@ -242,7 +246,10 @@ function createRedactor(rules) {
|
|
|
242
246
|
let error = event.error;
|
|
243
247
|
if (event.error !== void 0) {
|
|
244
248
|
const escName = applyShapeRules(event.error.name, compiled.shapeRules);
|
|
245
|
-
const escMessage = applyShapeRules(
|
|
249
|
+
const escMessage = applyShapeRules(
|
|
250
|
+
event.error.message,
|
|
251
|
+
compiled.shapeRules
|
|
252
|
+
);
|
|
246
253
|
const stack = event.error.stack;
|
|
247
254
|
const escStack = stack === void 0 ? void 0 : applyShapeRules(stack, compiled.shapeRules);
|
|
248
255
|
const errorChanged = escName !== event.error.name || escMessage !== event.error.message || escStack !== stack;
|
|
@@ -367,12 +374,13 @@ function isLogEventShape(value) {
|
|
|
367
374
|
return false;
|
|
368
375
|
}
|
|
369
376
|
const obj = value;
|
|
370
|
-
if (typeof obj
|
|
371
|
-
if (typeof obj
|
|
372
|
-
if (!VALID_LEVELS.has(obj
|
|
373
|
-
if (typeof obj
|
|
374
|
-
if (obj
|
|
375
|
-
|
|
377
|
+
if (typeof obj.timestamp !== "string") return false;
|
|
378
|
+
if (typeof obj.level !== "string") return false;
|
|
379
|
+
if (!VALID_LEVELS.has(obj.level)) return false;
|
|
380
|
+
if (typeof obj.message !== "string") return false;
|
|
381
|
+
if (obj.attributes === null || typeof obj.attributes !== "object")
|
|
382
|
+
return false;
|
|
383
|
+
if (obj.context === null || typeof obj.context !== "object") return false;
|
|
376
384
|
return true;
|
|
377
385
|
}
|
|
378
386
|
|
|
@@ -452,7 +460,8 @@ function sanitizeValueImpl(value, depth, ctx) {
|
|
|
452
460
|
if (depth > ctx.maxDepth) return "[MaxDepth]";
|
|
453
461
|
if (value === null) return null;
|
|
454
462
|
const t = typeof value;
|
|
455
|
-
if (t === "string")
|
|
463
|
+
if (t === "string")
|
|
464
|
+
return truncateString(value, ctx.maxStringLength);
|
|
456
465
|
if (t === "number") {
|
|
457
466
|
const n = value;
|
|
458
467
|
return Number.isFinite(n) ? n : null;
|
|
@@ -562,7 +571,7 @@ function sanitizeErrorAsAttribute(err, depth, ctx) {
|
|
|
562
571
|
};
|
|
563
572
|
const stack = safeOptional(() => err.stack);
|
|
564
573
|
if (stack !== void 0) {
|
|
565
|
-
reduced
|
|
574
|
+
reduced.stack = stack;
|
|
566
575
|
}
|
|
567
576
|
return sanitizeObject(reduced, depth, ctx);
|
|
568
577
|
}
|
|
@@ -683,7 +692,7 @@ var urlScrub = (event, _config) => {
|
|
|
683
692
|
const message = maybeScrubString(event.message);
|
|
684
693
|
const attributes = walkAttributes2(event.attributes);
|
|
685
694
|
const context = walkContext3(event.context);
|
|
686
|
-
let error
|
|
695
|
+
let error;
|
|
687
696
|
if (event.error !== void 0) {
|
|
688
697
|
const scrubbedMessage = maybeScrubString(event.error.message);
|
|
689
698
|
const stack = event.error.stack;
|
|
@@ -747,7 +756,7 @@ function scrubHashFragment(parsed, extras) {
|
|
|
747
756
|
}
|
|
748
757
|
}
|
|
749
758
|
if (!changed) return false;
|
|
750
|
-
parsed.hash =
|
|
759
|
+
parsed.hash = `#${out.join("&")}`;
|
|
751
760
|
return true;
|
|
752
761
|
}
|
|
753
762
|
function isDenied(name, extras) {
|
|
@@ -1110,6 +1119,36 @@ function installRuntime(runtime) {
|
|
|
1110
1119
|
return previous;
|
|
1111
1120
|
}
|
|
1112
1121
|
|
|
1122
|
+
// src/trace/validate.ts
|
|
1123
|
+
var MAX_TRACESTATE_LEN = 512;
|
|
1124
|
+
var TRACE_ID_RE = /^[0-9a-f]{32}$/;
|
|
1125
|
+
var SPAN_ID_RE = /^[0-9a-f]{16}$/;
|
|
1126
|
+
var ALL_ZERO_TRACE_ID = "0".repeat(32);
|
|
1127
|
+
var ALL_ZERO_SPAN_ID = "0".repeat(16);
|
|
1128
|
+
function isValidTraceId(value) {
|
|
1129
|
+
return typeof value === "string" && TRACE_ID_RE.test(value) && value !== ALL_ZERO_TRACE_ID;
|
|
1130
|
+
}
|
|
1131
|
+
function isValidSpanId(value) {
|
|
1132
|
+
return typeof value === "string" && SPAN_ID_RE.test(value) && value !== ALL_ZERO_SPAN_ID;
|
|
1133
|
+
}
|
|
1134
|
+
function normalizeTraceContext(candidate) {
|
|
1135
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
1136
|
+
return void 0;
|
|
1137
|
+
}
|
|
1138
|
+
const c = candidate;
|
|
1139
|
+
if (!isValidTraceId(c.traceId) || !isValidSpanId(c.spanId)) {
|
|
1140
|
+
return void 0;
|
|
1141
|
+
}
|
|
1142
|
+
const trace = { traceId: c.traceId, spanId: c.spanId };
|
|
1143
|
+
if (typeof c.traceFlags === "number" && Number.isInteger(c.traceFlags) && c.traceFlags >= 0 && c.traceFlags <= 255) {
|
|
1144
|
+
trace.traceFlags = c.traceFlags;
|
|
1145
|
+
}
|
|
1146
|
+
if (typeof c.traceState === "string" && c.traceState.length > 0 && c.traceState.length <= MAX_TRACESTATE_LEN) {
|
|
1147
|
+
trace.traceState = c.traceState;
|
|
1148
|
+
}
|
|
1149
|
+
return trace;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1113
1152
|
// src/api/logger.ts
|
|
1114
1153
|
var rootLogger;
|
|
1115
1154
|
function installState(config) {
|
|
@@ -1182,6 +1221,14 @@ function makeLogger(options, chainedContexts) {
|
|
|
1182
1221
|
...chainedContexts,
|
|
1183
1222
|
correlation
|
|
1184
1223
|
);
|
|
1224
|
+
if (context.trace !== void 0) {
|
|
1225
|
+
const normalized = normalizeTraceContext(context.trace);
|
|
1226
|
+
if (normalized === void 0) {
|
|
1227
|
+
delete context.trace;
|
|
1228
|
+
} else {
|
|
1229
|
+
context.trace = normalized;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1185
1232
|
const event = buildLogEvent({
|
|
1186
1233
|
level,
|
|
1187
1234
|
message,
|
|
@@ -1213,6 +1260,27 @@ function makeLogger(options, chainedContexts) {
|
|
|
1213
1260
|
};
|
|
1214
1261
|
}
|
|
1215
1262
|
|
|
1263
|
+
// src/trace/traceparent.ts
|
|
1264
|
+
var VERSION_RE = /^[0-9a-f]{2}$/;
|
|
1265
|
+
function parseTraceparent(traceparent, tracestate) {
|
|
1266
|
+
if (typeof traceparent !== "string") return void 0;
|
|
1267
|
+
const parts = traceparent.trim().split("-");
|
|
1268
|
+
if (parts.length < 4) return void 0;
|
|
1269
|
+
const [version, traceId, spanId, flagsHex] = parts;
|
|
1270
|
+
if (version === void 0 || !VERSION_RE.test(version)) return void 0;
|
|
1271
|
+
if (version === "ff") return void 0;
|
|
1272
|
+
if (flagsHex === void 0 || !/^[0-9a-f]{2}$/.test(flagsHex)) {
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
const traceFlags = Number.parseInt(flagsHex, 16);
|
|
1276
|
+
return normalizeTraceContext({
|
|
1277
|
+
traceId,
|
|
1278
|
+
spanId,
|
|
1279
|
+
traceFlags,
|
|
1280
|
+
...typeof tracestate === "string" ? { traceState: tracestate } : {}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1216
1284
|
// src/transport/console-transport.ts
|
|
1217
1285
|
function resolveConsoleMethod(level) {
|
|
1218
1286
|
const slot = console[level];
|
|
@@ -1235,6 +1303,7 @@ exports.configureLogging = configureLogging;
|
|
|
1235
1303
|
exports.createLogger = createLogger;
|
|
1236
1304
|
exports.createRedactor = createRedactor;
|
|
1237
1305
|
exports.getRootLogger = getRootLogger;
|
|
1306
|
+
exports.parseTraceparent = parseTraceparent;
|
|
1238
1307
|
exports.scrubUrl = scrubUrl;
|
|
1239
1308
|
//# sourceMappingURL=index.cjs.map
|
|
1240
1309
|
//# sourceMappingURL=index.cjs.map
|