@logbrew/sdk 0.1.0 → 0.1.1

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 CHANGED
@@ -7,36 +7,11 @@ Public JavaScript SDK for creating LogBrew event batches, validating them locall
7
7
  ```bash
8
8
  npm install @logbrew/sdk
9
9
  pnpm add @logbrew/sdk
10
- npm --prefix node_modules/@logbrew/sdk/examples run
11
- npm --prefix node_modules/@logbrew/sdk/examples run list
12
- npm --prefix node_modules/@logbrew/sdk/examples run help
13
- npm --prefix node_modules/@logbrew/sdk/examples run readme-example
14
- npm --prefix node_modules/@logbrew/sdk/examples run readme-example:cjs
15
- npm --prefix node_modules/@logbrew/sdk/examples run real-user-smoke
16
- npm --prefix node_modules/@logbrew/sdk/examples run real-user-smoke:cjs
17
- pnpm --dir node_modules/@logbrew/sdk/examples run
18
- pnpm --dir node_modules/@logbrew/sdk/examples run list
19
- pnpm --dir node_modules/@logbrew/sdk/examples run help
20
- pnpm --dir node_modules/@logbrew/sdk/examples run readme-example
21
- pnpm --dir node_modules/@logbrew/sdk/examples run readme-example:cjs
22
- pnpm --dir node_modules/@logbrew/sdk/examples run real-user-smoke
23
- pnpm --dir node_modules/@logbrew/sdk/examples run real-user-smoke:cjs
24
- node node_modules/@logbrew/sdk/examples/index.mjs --help
25
- node node_modules/@logbrew/sdk/examples/index.mjs --list
26
- node node_modules/@logbrew/sdk/examples/index.mjs readme-example
27
- node node_modules/@logbrew/sdk/examples/index.mjs readme-example:cjs
28
- node node_modules/@logbrew/sdk/examples/index.mjs real-user-smoke
29
- node node_modules/@logbrew/sdk/examples/index.mjs
30
- node node_modules/@logbrew/sdk/examples/index.mjs real-user-smoke:cjs
31
- node node_modules/@logbrew/sdk/examples/readme-example.mjs
32
- node node_modules/@logbrew/sdk/examples/readme-example.cjs
33
- node node_modules/@logbrew/sdk/examples/real-user-smoke.mjs
34
- node node_modules/@logbrew/sdk/examples/real-user-smoke.cjs
35
10
  ```
36
11
 
37
12
  The package supports both ESM `import` and CommonJS `require`.
38
13
  The shipped package also includes `.d.ts` and `.d.cts` declarations so ESM and CommonJS TypeScript consumers can install it directly without a separate build step.
39
- The packed tarball and installed package both ship `README.md`, package metadata, an explicit ESM/CommonJS `exports` map, runnable `examples/readme-example.mjs`, `examples/readme-example.cjs`, `examples/real-user-smoke.mjs`, and `examples/real-user-smoke.cjs` files, a tiny `examples/package.json` helper surface, a packaged `examples/index.mjs` launcher, declaration comments in `index.d.ts`, and the CommonJS `index.d.cts` declaration path that TypeScript-aware tools can inspect. Those packed and installed README, example, and declaration surfaces should still include the normal `npm` and `pnpm` install commands, the fake `LOGBREW_API_KEY` placeholder guidance, the `previewJson()` usage note, the optional `parseTraceparent()`, `createTraceparent()`, `spanAttributesFromTraceparent()`, `installLogBrewConsoleCapture()`, `createLogBrewPinoDestination()`, and `createLogBrewWinstonTransport()` guidance, the helper discovery commands, helper runtime commands, and launcher commands shown above, and the typed API docs for shapes like `ReleaseAttributes`, `SpanAttributes`, `TraceparentContext`, `TraceparentInput`, `TraceparentSpanInput`, `Event`, `ConsoleCaptureConfig`, `ConsoleCaptureHandle`, `PinoDestinationConfig`, `PinoDestinationHandle`, `PinoLogRecord`, `WinstonTransportConfig`, `WinstonTransportHandle`, `WinstonLogInfo`, `SdkError`, `TransportError`, `TransportResponse`, `alwaysAccept()`, `lastBody()`, `pendingEvents()`, `flush()`, and `shutdown()`. Real installs should also start from a package-manager-native app bootstrap, using `npm init -y` on the npm path and `pnpm init` on the pnpm path, then rewrite the temp app `package.json` dependencies plus the lockfiles so they point back to the packed `.tgz` with the expected file target and integrity metadata, while npm’s own `npm pack --dry-run --json` and `npm pack --json` outputs still report the expected tarball name, integrity, shasum, and file list, and those generated lockfiles should retain that same tarball integrity value before recreating the install through `npm ci` and `pnpm install --frozen-lockfile`. The temp app should also survive a package-manager-native removal step, with `npm uninstall @logbrew/sdk` and `pnpm remove @logbrew/sdk` dropping the SDK from `package.json`, the top-level installed-package list, and `node_modules` before the tarball is added back. The consumer graph should also stay explicit in package-manager-native output, with plain `npm ls @logbrew/sdk` and `pnpm ls @logbrew/sdk` showing the direct installed SDK tree a user reads in the terminal, `npm explain @logbrew/sdk` showing the direct root-project edge, `pnpm why @logbrew/sdk` showing the temp app dependency relationship, plain `npm list --depth=0` and `pnpm list --depth=0` showing the top-level installed dependency summary, and `npm list --json --depth=0` plus `pnpm list --json --depth=0` still listing the temp app and direct installed dependencies, while small installed-user script entries in the temp app still exercise the shipped package through `npm run` and `pnpm run`, including a TypeScript typecheck script that compiles both an ESM `.ts` consumer and a CommonJS `.cts` consumer plus a script that mirrors the published README example before and after reinstall. The installed package should also let a consumer run the shipped README-style and stronger happy-path ESM and CommonJS example files directly from `node_modules/@logbrew/sdk/examples/`, and it now ships a small Node launcher at `node node_modules/@logbrew/sdk/examples/index.mjs` so users can discover packaged examples through `--help`, list them through `--list`, run the default no-argument `real-user-smoke` path, or select named examples like `readme-example` and `real-user-smoke` without going through package-manager scripts. The helper commands in `node_modules/@logbrew/sdk/examples/package.json` should still be discoverable enough that plain `npm --prefix node_modules/@logbrew/sdk/examples run` and `pnpm --dir node_modules/@logbrew/sdk/examples run` list the available example entrypoints before users run `npm --prefix node_modules/@logbrew/sdk/examples run help`, where that help output should print copy-pasteable installed-user commands for both npm and pnpm across the README and `real-user-smoke` default, ESM, and CommonJS example paths, including `npm --prefix node_modules/@logbrew/sdk/examples run readme-example` or `pnpm --dir node_modules/@logbrew/sdk/examples run readme-example`, `npm --prefix node_modules/@logbrew/sdk/examples run readme-example:esm` or `pnpm --dir node_modules/@logbrew/sdk/examples run readme-example:esm`, `npm --prefix node_modules/@logbrew/sdk/examples run readme-example:cjs` or `pnpm --dir node_modules/@logbrew/sdk/examples run readme-example:cjs`, `npm --prefix node_modules/@logbrew/sdk/examples run real-user-smoke` or `pnpm --dir node_modules/@logbrew/sdk/examples run real-user-smoke`, `npm --prefix node_modules/@logbrew/sdk/examples run real-user-smoke:esm` or `pnpm --dir node_modules/@logbrew/sdk/examples run real-user-smoke:esm`, and `npm --prefix node_modules/@logbrew/sdk/examples run real-user-smoke:cjs` or `pnpm --dir node_modules/@logbrew/sdk/examples run real-user-smoke:cjs`, while the launcher surface itself should still reveal `node node_modules/@logbrew/sdk/examples/index.mjs --help`, `node node_modules/@logbrew/sdk/examples/index.mjs --list`, `node node_modules/@logbrew/sdk/examples/index.mjs`, `node node_modules/@logbrew/sdk/examples/index.mjs readme-example`, `node node_modules/@logbrew/sdk/examples/index.mjs real-user-smoke`, and `node node_modules/@logbrew/sdk/examples/index.mjs real-user-smoke:cjs`.
14
+ The package ships copyable examples under `node_modules/@logbrew/sdk/examples/`. Use the fake `LOGBREW_API_KEY` placeholder in docs, keep the real key in your app configuration, and call `previewJson()` when you want to inspect queued JSON before sending. Type declarations document payload shapes such as `ReleaseAttributes`, `SpanAttributes`, `MetricAttributes`, transport responses, SDK errors, lifecycle helpers, W3C trace helpers, product timeline helpers, console capture, Pino destination, and Winston transport APIs.
40
15
 
41
16
  ## Example
42
17
 
@@ -87,13 +62,30 @@ const response = await client.shutdown(transport);
87
62
  console.error(JSON.stringify({ ok: true, status: response.statusCode, attempts: response.attempts, events: 6 }));
88
63
  ```
89
64
 
65
+ ## Explicit Metrics
66
+
67
+ Use `client.metric()` when application code already knows the measurement name, value, unit, and aggregation shape. Metrics are queued like other events and are not collected automatically.
68
+
69
+ ```js
70
+ client.metric("evt_metric_001", "2026-06-02T10:00:06Z", {
71
+ name: "checkout.requests",
72
+ kind: "counter",
73
+ value: 42,
74
+ unit: "{request}",
75
+ temporality: "delta",
76
+ metadata: { service: "checkout" }
77
+ });
78
+ ```
79
+
80
+ Metric `kind` must be `counter`, `gauge`, or `histogram`. Counters and histograms must be non-negative and use `delta` or `cumulative` temporality; gauges use `instant` temporality and may be negative. Keep metric metadata primitive and low-cardinality, such as service, region, or route template.
81
+
90
82
  ## W3C Trace Context
91
83
 
92
84
  Use `parseTraceparent()`, `createTraceparent()`, and `spanAttributesFromTraceparent()` when a JavaScript service needs to continue trace context from OpenTelemetry-compatible services or pass a W3C `traceparent` value downstream.
93
85
 
94
86
  ```js
95
87
  import {
96
- createTraceparent,
88
+ createTraceparentHeaders,
97
89
  LogBrewClient,
98
90
  RecordingTransport,
99
91
  spanAttributesFromTraceparent
@@ -115,19 +107,61 @@ const span = spanAttributesFromTraceparent(incomingTraceparent, {
115
107
  });
116
108
  client.span("evt_checkout_span", "2026-06-02T10:00:04Z", span);
117
109
 
118
- const downstreamTraceparent = createTraceparent({
119
- traceId: span.traceId,
120
- spanId: span.spanId,
121
- traceFlags: "01"
122
- });
123
110
  await fetch("https://example.invalid/payments", {
124
- headers: { traceparent: downstreamTraceparent }
111
+ headers: createTraceparentHeaders({
112
+ traceId: span.traceId,
113
+ spanId: span.spanId,
114
+ traceFlags: "01"
115
+ })
125
116
  });
126
117
 
127
118
  await client.flush(RecordingTransport.alwaysAccept());
128
119
  ```
129
120
 
130
- The helpers validate the W3C `version-traceId-parentSpanId-traceFlags` shape, reject all-zero trace/span ids, normalize valid ids to lowercase, expose the sampled flag from `traceFlags`, and keep span metadata primitive-only. They do not install OpenTelemetry or patch HTTP clients; use them when you need explicit interop in code you own.
121
+ The helpers validate the W3C `version-traceId-parentSpanId-traceFlags` shape, reject all-zero trace/span ids, normalize valid ids to lowercase, expose the sampled flag from `traceFlags`, and keep span metadata primitive-only. `createTraceparentHeaders()` returns an explicit outbound carrier with only `traceparent`. The helpers do not install OpenTelemetry or patch HTTP clients; use them when you need explicit interop in code you own.
122
+
123
+ LogBrew severity categories are `info`, `warning`, `error`, and `critical`. The JavaScript SDK accepts common runtime aliases such as `trace`, `debug`, `warn`, and `fatal` for compatibility, then serializes canonical values before queued events are sent.
124
+
125
+ ## Agent-Readable Timelines
126
+
127
+ Use `createProductActionAttributes()` and `createNetworkMilestoneAttributes()` when a service already knows important product steps or API milestones. The helpers create normal `action` event attributes with primitive metadata that can be analyzed across many sessions without visual replay, global HTTP patching, payload capture, or header capture.
128
+
129
+ ```js
130
+ import {
131
+ createNetworkMilestoneAttributes,
132
+ createProductActionAttributes,
133
+ LogBrewClient
134
+ } from "@logbrew/sdk";
135
+
136
+ const client = LogBrewClient.create({
137
+ apiKey: "LOGBREW_API_KEY",
138
+ sdkName: "checkout-api",
139
+ sdkVersion: "1.0.0"
140
+ });
141
+
142
+ client.action("evt_checkout_submit", new Date().toISOString(), createProductActionAttributes({
143
+ name: "checkout.submit",
144
+ status: "running",
145
+ sessionId: "sess_123",
146
+ traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
147
+ routeTemplate: "/checkout/:step",
148
+ funnel: "checkout",
149
+ step: "submit",
150
+ metadata: { service: "checkout" }
151
+ }));
152
+
153
+ client.action("evt_payment_api", new Date().toISOString(), createNetworkMilestoneAttributes({
154
+ routeTemplate: "/payments/:id",
155
+ method: "POST",
156
+ statusCode: 202,
157
+ durationMs: 94,
158
+ sessionId: "sess_123",
159
+ traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
160
+ metadata: { service: "checkout" }
161
+ }));
162
+ ```
163
+
164
+ Timeline helpers keep only primitive metadata, strip query strings and hashes from route templates, normalize HTTP methods, infer failed network milestones from status codes `400` and above, and serialize through the existing `action` event type. Keep metadata low-cardinality, such as `sessionId`, `traceId`, `routeTemplate`, `method`, `statusCode`, `durationMs`, `screen`, `funnel`, and `step`.
131
165
 
132
166
  ## Console Capture
133
167
 
@@ -186,7 +220,7 @@ logger.error(new Error("payment failed"), "checkout failed");
186
220
  await destination.flush();
187
221
  ```
188
222
 
189
- The Pino adapter reads JSON log lines, maps `trace`/`debug`/`info`/`warn`/`error`/`fatal` into LogBrew levels, captures primitive Pino fields as `context.*`, captures serialized error name/message, skips noisy runtime defaults, and omits stack text unless `includeErrorStack: true` is set. It does not patch Pino or replace application logger ownership.
223
+ The Pino adapter reads JSON log lines, maps Pino `trace`/`debug` to LogBrew `info`, `warn` to `warning`, `error` to `error`, and `fatal` to `critical`, captures primitive Pino fields as `context.*`, captures serialized error name/message, skips noisy runtime defaults, and omits stack text unless `includeErrorStack: true` is set. It does not patch Pino or replace application logger ownership.
190
224
 
191
225
  ## Winston Transport
192
226
 
@@ -220,6 +254,6 @@ logger.error(new Error("payment failed"));
220
254
  await logbrewTransport.flush();
221
255
  ```
222
256
 
223
- The Winston adapter receives Winston `info` objects, maps `debug`/`silly` to `debug`, `warn` to `warning`, `error`/`fatal` to `error`, and other common Winston levels to `info`. It captures primitive info fields as `context.*`, captures nested `err`/`error` objects or formatted error stack name/message, omits stack text unless `includeErrorStack: true` is set, and exposes `onError` for capture failures. It does not mutate Winston globals or replace the app's logger.
257
+ The Winston adapter receives Winston `info` objects, maps `debug`/`silly` to LogBrew `info`, `warn` to `warning`, `error` to `error`, `fatal`/`critical` to `critical`, and other common Winston levels to `info`. It captures primitive info fields as `context.*`, captures nested `err`/`error` objects or formatted error stack name/message, omits stack text unless `includeErrorStack: true` is set, and exposes `onError` for capture failures. It does not mutate Winston globals or replace the app's logger.
224
258
 
225
- Use a clearly fake placeholder like `LOGBREW_API_KEY` in local examples and tests. Call `flush` or `shutdown` to send queued events through a transport, and use `previewJson()` when you want a stable local JSON preview without sending anything.
259
+ Use a clearly fake placeholder like `LOGBREW_API_KEY` in examples. Call `flush` or `shutdown` to send queued events through a transport, and use `previewJson()` when you want a stable local JSON preview before sending anything.
@@ -9,7 +9,16 @@ try {
9
9
  }
10
10
  }
11
11
 
12
- const { LogBrewClient, RecordingTransport } = sdk;
12
+ const { createTraceparentHeaders, LogBrewClient, RecordingTransport } = sdk;
13
+
14
+ const outgoingHeaders = createTraceparentHeaders({
15
+ traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
16
+ spanId: "b7ad6b7169203331",
17
+ traceFlags: "01"
18
+ });
19
+ if (outgoingHeaders.traceparent !== "00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01") {
20
+ throw new Error("createTraceparentHeaders produced an unexpected carrier");
21
+ }
13
22
 
14
23
  const client = LogBrewClient.create({
15
24
  apiKey: "LOGBREW_API_KEY",
@@ -5,7 +5,16 @@ const sdk = await import("@logbrew/sdk").catch(async (error) => {
5
5
  throw error;
6
6
  });
7
7
 
8
- const { LogBrewClient, RecordingTransport } = sdk;
8
+ const { createTraceparentHeaders, LogBrewClient, RecordingTransport } = sdk;
9
+
10
+ const outgoingHeaders = createTraceparentHeaders({
11
+ traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
12
+ spanId: "b7ad6b7169203331",
13
+ traceFlags: "01"
14
+ });
15
+ if (outgoingHeaders.traceparent !== "00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01") {
16
+ throw new Error("createTraceparentHeaders produced an unexpected carrier");
17
+ }
9
18
 
10
19
  const client = LogBrewClient.create({
11
20
  apiKey: "LOGBREW_API_KEY",
package/index.cjs CHANGED
@@ -1,7 +1,23 @@
1
- const ISSUE_LEVELS = new Set(["info", "warning", "error", "critical"]);
2
- const LOG_LEVELS = new Set(["debug", "info", "warning", "error"]);
1
+ const SEVERITY_ALIASES = new Map([
2
+ ["trace", "info"],
3
+ ["debug", "info"],
4
+ ["info", "info"],
5
+ ["warn", "warning"],
6
+ ["warning", "warning"],
7
+ ["error", "error"],
8
+ ["fatal", "critical"],
9
+ ["critical", "critical"]
10
+ ]);
11
+ const SEVERITY_VALUES = new Set(SEVERITY_ALIASES.keys());
3
12
  const SPAN_STATUSES = new Set(["ok", "error"]);
4
13
  const ACTION_STATUSES = new Set(["queued", "running", "success", "failure"]);
14
+ const METRIC_KINDS = new Set(["counter", "gauge", "histogram"]);
15
+ const NON_NEGATIVE_METRIC_KINDS = new Set(["counter", "histogram"]);
16
+ const METRIC_TEMPORALITIES_BY_KIND = new Map([
17
+ ["counter", new Set(["delta", "cumulative"])],
18
+ ["gauge", new Set(["instant"])],
19
+ ["histogram", new Set(["delta", "cumulative"])]
20
+ ]);
5
21
  const CONSOLE_METHODS = new Set(["debug", "info", "log", "warn", "error"]);
6
22
  const DEFAULT_CONSOLE_LEVELS = ["debug", "info", "log", "warn", "error"];
7
23
  const PINO_HOST_FIELD = ["host", "name"].join("");
@@ -119,6 +135,10 @@ class LogBrewClient {
119
135
  this.#pushEvent("action", id, timestamp, validateAction(attributes));
120
136
  }
121
137
 
138
+ metric(id, timestamp, attributes) {
139
+ this.#pushEvent("metric", id, timestamp, validateMetric(attributes));
140
+ }
141
+
122
142
  async flush(transport) {
123
143
  if (this.closed) {
124
144
  throw new SdkError("shutdown_error", "client is already shut down");
@@ -326,7 +346,7 @@ function logAttributesFromConsoleArgs(method, args, options = {}) {
326
346
  function logbrewLevelFromConsoleMethod(method) {
327
347
  switch (method) {
328
348
  case "debug":
329
- return "debug";
349
+ return "info";
330
350
  case "warn":
331
351
  return "warning";
332
352
  case "error":
@@ -339,6 +359,44 @@ function logbrewLevelFromConsoleMethod(method) {
339
359
  }
340
360
  }
341
361
 
362
+ function createProductActionAttributes(action, options = {}) {
363
+ const details = productActionDetails(action);
364
+ return {
365
+ name: details.name,
366
+ status: details.status,
367
+ metadata: compactMetadata({
368
+ source: "product.action",
369
+ ...compactMetadata(options.metadata),
370
+ ...compactMetadata(details.metadata),
371
+ routeTemplate: sanitizeRouteTemplate(details.routeTemplate),
372
+ sessionId: stringOrUndefined(details.sessionId),
373
+ traceId: stringOrUndefined(details.traceId),
374
+ screen: stringOrUndefined(details.screen),
375
+ funnel: stringOrUndefined(details.funnel),
376
+ step: stringOrUndefined(details.step)
377
+ })
378
+ };
379
+ }
380
+
381
+ function createNetworkMilestoneAttributes(request, options = {}) {
382
+ const details = networkMilestoneDetails(request);
383
+ return {
384
+ name: details.name,
385
+ status: details.status,
386
+ metadata: compactMetadata({
387
+ source: "network.milestone",
388
+ ...compactMetadata(options.metadata),
389
+ ...compactMetadata(details.metadata),
390
+ routeTemplate: details.routeTemplate,
391
+ method: details.method,
392
+ statusCode: details.statusCode,
393
+ durationMs: details.durationMs,
394
+ sessionId: stringOrUndefined(details.sessionId),
395
+ traceId: stringOrUndefined(details.traceId)
396
+ })
397
+ };
398
+ }
399
+
342
400
  function parseTraceparent(traceparent) {
343
401
  if (typeof traceparent !== "string" || traceparent.trim() === "") {
344
402
  throw new SdkError("validation_error", "traceparent must be non-empty");
@@ -379,6 +437,10 @@ function createTraceparent({ traceId, spanId, traceFlags = "01" }) {
379
437
  return `00-${traceId.toLowerCase()}-${spanId.toLowerCase()}-${traceFlags.toLowerCase()}`;
380
438
  }
381
439
 
440
+ function createTraceparentHeaders(input) {
441
+ return { traceparent: createTraceparent(input) };
442
+ }
443
+
382
444
  function spanAttributesFromTraceparent(traceparent, attributes) {
383
445
  if (!attributes || Array.isArray(attributes) || typeof attributes !== "object") {
384
446
  throw new SdkError("validation_error", "span attributes must be an object");
@@ -646,15 +708,16 @@ function logbrewLevelFromWinstonLevel(level) {
646
708
  switch (String(level).toLowerCase()) {
647
709
  case "debug":
648
710
  case "silly":
649
- return "debug";
711
+ return "info";
650
712
  case "warn":
651
713
  case "warning":
652
714
  return "warning";
653
715
  case "error":
716
+ return "error";
654
717
  case "crit":
655
718
  case "critical":
656
719
  case "fatal":
657
- return "error";
720
+ return "critical";
658
721
  case "http":
659
722
  case "verbose":
660
723
  case "info":
@@ -757,6 +820,9 @@ function timestampFromPinoRecord(record, fallbackTimestamp) {
757
820
 
758
821
  function logbrewLevelFromPinoLevel(level) {
759
822
  if (typeof level === "number" && Number.isFinite(level)) {
823
+ if (level >= 60) {
824
+ return "critical";
825
+ }
760
826
  if (level >= 50) {
761
827
  return "error";
762
828
  }
@@ -766,19 +832,21 @@ function logbrewLevelFromPinoLevel(level) {
766
832
  if (level >= 30) {
767
833
  return "info";
768
834
  }
769
- return "debug";
835
+ return "info";
770
836
  }
771
837
 
772
838
  switch (String(level).toLowerCase()) {
773
839
  case "trace":
774
840
  case "debug":
775
- return "debug";
841
+ return "info";
776
842
  case "warn":
777
843
  case "warning":
778
844
  return "warning";
779
845
  case "error":
780
- case "fatal":
781
846
  return "error";
847
+ case "fatal":
848
+ case "critical":
849
+ return "critical";
782
850
  case "info":
783
851
  default:
784
852
  return "info";
@@ -881,6 +949,12 @@ function requireAllowedValue(label, value, allowedValues) {
881
949
  }
882
950
  }
883
951
 
952
+ function requireFiniteNumber(label, value) {
953
+ if (typeof value !== "number" || !Number.isFinite(value)) {
954
+ throw new SdkError("validation_error", `${label} must be a finite number`);
955
+ }
956
+ }
957
+
884
958
  function requireTraceId(traceId) {
885
959
  if (typeof traceId !== "string" || !/^[0-9a-fA-F]{32}$/u.test(traceId)) {
886
960
  throw new SdkError("validation_error", "traceId must be 32 lowercase or uppercase hex characters");
@@ -952,24 +1026,29 @@ function validateEnvironment(attributes) {
952
1026
 
953
1027
  function validateIssue(attributes) {
954
1028
  requireNonEmpty("issue title", attributes.title);
955
- requireAllowedValue("issue level", attributes.level, ISSUE_LEVELS);
1029
+ const level = normalizeSeverity("issue level", attributes.level);
956
1030
  return withMetadata({
957
1031
  title: attributes.title,
958
- level: attributes.level,
1032
+ level,
959
1033
  ...(attributes.message !== undefined ? { message: attributes.message } : {})
960
1034
  }, attributes.metadata);
961
1035
  }
962
1036
 
963
1037
  function validateLog(attributes) {
964
1038
  requireNonEmpty("log message", attributes.message);
965
- requireAllowedValue("log level", attributes.level, LOG_LEVELS);
1039
+ const level = normalizeSeverity("log level", attributes.level);
966
1040
  return withMetadata({
967
1041
  message: attributes.message,
968
- level: attributes.level,
1042
+ level,
969
1043
  ...(attributes.logger !== undefined ? { logger: attributes.logger } : {})
970
1044
  }, attributes.metadata);
971
1045
  }
972
1046
 
1047
+ function normalizeSeverity(label, value) {
1048
+ requireAllowedValue(label, value, SEVERITY_VALUES);
1049
+ return SEVERITY_ALIASES.get(value);
1050
+ }
1051
+
973
1052
  function validateSpan(attributes) {
974
1053
  requireNonEmpty("span name", attributes.name);
975
1054
  requireNonEmpty("span traceId", attributes.traceId);
@@ -1002,6 +1081,146 @@ function validateAction(attributes) {
1002
1081
  }, attributes.metadata);
1003
1082
  }
1004
1083
 
1084
+ function validateMetric(attributes) {
1085
+ requireNonEmpty("metric name", attributes.name);
1086
+ requireAllowedValue("metric kind", attributes.kind, METRIC_KINDS);
1087
+ requireFiniteNumber("metric value", attributes.value);
1088
+ requireNonEmpty("metric unit", attributes.unit);
1089
+
1090
+ const allowedTemporalities = METRIC_TEMPORALITIES_BY_KIND.get(attributes.kind);
1091
+ requireAllowedValue(`metric temporality for ${attributes.kind}`, attributes.temporality, allowedTemporalities);
1092
+ if (NON_NEGATIVE_METRIC_KINDS.has(attributes.kind) && attributes.value < 0) {
1093
+ throw new SdkError("validation_error", `metric ${attributes.kind} value must be non-negative`);
1094
+ }
1095
+
1096
+ return withMetadata({
1097
+ name: attributes.name,
1098
+ kind: attributes.kind,
1099
+ value: attributes.value,
1100
+ unit: attributes.unit,
1101
+ temporality: attributes.temporality
1102
+ }, attributes.metadata);
1103
+ }
1104
+
1105
+ function productActionDetails(action) {
1106
+ if (typeof action === "string") {
1107
+ return { name: action, status: "success" };
1108
+ }
1109
+ if (!action || Array.isArray(action) || typeof action !== "object") {
1110
+ throw new SdkError("validation_error", "product action must be a string or object");
1111
+ }
1112
+ requireNonEmpty("product action name", action.name);
1113
+ const status = action.status === undefined ? "success" : action.status;
1114
+ requireAllowedValue("product action status", status, ACTION_STATUSES);
1115
+ return {
1116
+ funnel: action.funnel,
1117
+ metadata: action.metadata,
1118
+ name: action.name,
1119
+ routeTemplate: action.routeTemplate,
1120
+ screen: action.screen,
1121
+ sessionId: action.sessionId,
1122
+ status,
1123
+ step: action.step,
1124
+ traceId: action.traceId
1125
+ };
1126
+ }
1127
+
1128
+ function networkMilestoneDetails(request) {
1129
+ if (typeof request === "string") {
1130
+ return networkMilestoneDetails({ routeTemplate: request });
1131
+ }
1132
+ if (!request || Array.isArray(request) || typeof request !== "object") {
1133
+ throw new SdkError("validation_error", "network milestone must be a string or object");
1134
+ }
1135
+
1136
+ const routeTemplate = sanitizeRouteTemplate(request.routeTemplate);
1137
+ requireNonEmpty("network milestone routeTemplate", routeTemplate);
1138
+ const method = normalizeHttpMethod(request.method);
1139
+ const statusCode = statusCodeOrUndefined(request.statusCode);
1140
+ const status = request.status === undefined
1141
+ ? statusFromStatusCode(statusCode)
1142
+ : request.status;
1143
+ requireAllowedValue("network milestone status", status, ACTION_STATUSES);
1144
+ const durationMs = nonNegativeNumberOrUndefined("network milestone durationMs", request.durationMs);
1145
+ const name = typeof request.name === "string" && request.name.trim() !== ""
1146
+ ? request.name
1147
+ : `network.${method.toLowerCase()} ${routeTemplate}`;
1148
+
1149
+ return {
1150
+ durationMs,
1151
+ metadata: request.metadata,
1152
+ method,
1153
+ name,
1154
+ routeTemplate,
1155
+ sessionId: request.sessionId,
1156
+ status,
1157
+ statusCode,
1158
+ traceId: request.traceId
1159
+ };
1160
+ }
1161
+
1162
+ function sanitizeRouteTemplate(routeTemplate) {
1163
+ if (routeTemplate === undefined) {
1164
+ return undefined;
1165
+ }
1166
+ if (typeof routeTemplate !== "string") {
1167
+ throw new SdkError("validation_error", "routeTemplate must be a string");
1168
+ }
1169
+ const trimmed = routeTemplate.trim();
1170
+ if (trimmed === "") {
1171
+ return "";
1172
+ }
1173
+ try {
1174
+ const url = new URL(trimmed, "https://logbrew.example");
1175
+ return url.pathname || "/";
1176
+ } catch {
1177
+ return trimmed.split(/[?#]/u)[0] || "/";
1178
+ }
1179
+ }
1180
+
1181
+ function normalizeHttpMethod(method) {
1182
+ const value = method === undefined ? "GET" : method;
1183
+ if (typeof value !== "string" || value.trim() === "") {
1184
+ throw new SdkError("validation_error", "network milestone method must be a non-empty string");
1185
+ }
1186
+ const normalized = value.trim().toUpperCase();
1187
+ if (!/^[A-Z][A-Z0-9_-]*$/u.test(normalized)) {
1188
+ throw new SdkError("validation_error", "network milestone method must be a valid HTTP method");
1189
+ }
1190
+ return normalized;
1191
+ }
1192
+
1193
+ function statusCodeOrUndefined(value) {
1194
+ if (value === undefined) {
1195
+ return undefined;
1196
+ }
1197
+ if (!Number.isInteger(value) || value < 100 || value > 599) {
1198
+ throw new SdkError("validation_error", "network milestone statusCode must be an integer from 100 to 599");
1199
+ }
1200
+ return value;
1201
+ }
1202
+
1203
+ function statusFromStatusCode(statusCode) {
1204
+ if (statusCode !== undefined && statusCode >= 400) {
1205
+ return "failure";
1206
+ }
1207
+ return "success";
1208
+ }
1209
+
1210
+ function nonNegativeNumberOrUndefined(label, value) {
1211
+ if (value === undefined) {
1212
+ return undefined;
1213
+ }
1214
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
1215
+ throw new SdkError("validation_error", `${label} must be a non-negative number`);
1216
+ }
1217
+ return value;
1218
+ }
1219
+
1220
+ function stringOrUndefined(value) {
1221
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
1222
+ }
1223
+
1005
1224
  function withMetadata(attributes, metadata) {
1006
1225
  const safeMetadata = cloneMetadata(metadata);
1007
1226
  return safeMetadata === undefined
@@ -1082,7 +1301,10 @@ function formatConsoleArgument(value, includeErrorStack) {
1082
1301
  }
1083
1302
 
1084
1303
  module.exports = {
1304
+ createNetworkMilestoneAttributes,
1305
+ createProductActionAttributes,
1085
1306
  createTraceparent,
1307
+ createTraceparentHeaders,
1086
1308
  createLogBrewPinoDestination,
1087
1309
  createLogBrewWinstonTransport,
1088
1310
  installLogBrewConsoleCapture,
package/index.d.cts CHANGED
@@ -2,6 +2,12 @@
2
2
  export type MetadataValue = string | number | boolean | null;
3
3
  /** Structured metadata map shared by public LogBrew event attribute types. */
4
4
  export type Metadata = Record<string, MetadataValue>;
5
+ /** Canonical user-facing severity categories accepted by LogBrew. */
6
+ export type Severity = "info" | "warning" | "error" | "critical";
7
+ /** Runtime-level aliases accepted for compatibility and normalized before send. */
8
+ export type SeverityAlias = "trace" | "debug" | "warn" | "fatal";
9
+ /** Public severity input accepted by issue and log attributes. */
10
+ export type SeverityInput = Severity | SeverityAlias;
5
11
 
6
12
  /** Parsed W3C trace context from a traceparent value. */
7
13
  export type TraceparentContext = {
@@ -46,7 +52,7 @@ export type EnvironmentAttributes = {
46
52
  /** Public issue event attributes. */
47
53
  export type IssueAttributes = {
48
54
  title: string;
49
- level: "info" | "warning" | "error" | "critical";
55
+ level: SeverityInput;
50
56
  message?: string;
51
57
  metadata?: Metadata;
52
58
  };
@@ -54,7 +60,7 @@ export type IssueAttributes = {
54
60
  /** Public log event attributes. */
55
61
  export type LogAttributes = {
56
62
  message: string;
57
- level: "debug" | "info" | "warning" | "error";
63
+ level: SeverityInput;
58
64
  logger?: string;
59
65
  metadata?: Metadata;
60
66
  };
@@ -177,6 +183,54 @@ export type ActionAttributes = {
177
183
  metadata?: Metadata;
178
184
  };
179
185
 
186
+ /** App-owned product step input for agent-readable action timelines. */
187
+ export type ProductActionInput = string | {
188
+ name: string;
189
+ status?: ActionAttributes["status"];
190
+ sessionId?: string;
191
+ traceId?: string;
192
+ routeTemplate?: string;
193
+ screen?: string;
194
+ funnel?: string;
195
+ step?: string;
196
+ metadata?: Metadata;
197
+ };
198
+
199
+ /** App-owned API milestone input for agent-readable network timelines. */
200
+ export type NetworkMilestoneInput = string | {
201
+ name?: string;
202
+ routeTemplate: string;
203
+ method?: string;
204
+ status?: ActionAttributes["status"];
205
+ statusCode?: number;
206
+ durationMs?: number;
207
+ sessionId?: string;
208
+ traceId?: string;
209
+ metadata?: Metadata;
210
+ };
211
+
212
+ /** Shared timeline helper options for primitive app metadata. */
213
+ export type TimelineAttributesOptions = {
214
+ metadata?: Metadata;
215
+ };
216
+
217
+ /** Public metric event attributes. Use low-cardinality metadata only. */
218
+ export type MetricAttributes = {
219
+ name: string;
220
+ kind: "counter" | "histogram";
221
+ value: number;
222
+ unit: string;
223
+ temporality: "delta" | "cumulative";
224
+ metadata?: Metadata;
225
+ } | {
226
+ name: string;
227
+ kind: "gauge";
228
+ value: number;
229
+ unit: string;
230
+ temporality: "instant";
231
+ metadata?: Metadata;
232
+ };
233
+
180
234
  /** Public event union used in preview and transport payloads. */
181
235
  export type Event =
182
236
  | { type: "release"; id: string; timestamp: string; attributes: ReleaseAttributes }
@@ -184,7 +238,8 @@ export type Event =
184
238
  | { type: "issue"; id: string; timestamp: string; attributes: IssueAttributes }
185
239
  | { type: "log"; id: string; timestamp: string; attributes: LogAttributes }
186
240
  | { type: "span"; id: string; timestamp: string; attributes: SpanAttributes }
187
- | { type: "action"; id: string; timestamp: string; attributes: ActionAttributes };
241
+ | { type: "action"; id: string; timestamp: string; attributes: ActionAttributes }
242
+ | { type: "metric"; id: string; timestamp: string; attributes: MetricAttributes };
188
243
 
189
244
  /** Stable transport response returned from flush and shutdown operations. */
190
245
  export type TransportResponse = {
@@ -245,6 +300,7 @@ export declare class LogBrewClient {
245
300
  log(id: string, timestamp: string, attributes: LogAttributes): void;
246
301
  span(id: string, timestamp: string, attributes: SpanAttributes): void;
247
302
  action(id: string, timestamp: string, attributes: ActionAttributes): void;
303
+ metric(id: string, timestamp: string, attributes: MetricAttributes): void;
248
304
  /** Flush queued events through a transport while preserving retry semantics. */
249
305
  flush(transport: Transport): Promise<TransportResponse>;
250
306
  /** Flush queued events, then mark the client closed so later writes fail. */
@@ -254,6 +310,18 @@ export declare class LogBrewClient {
254
310
  /** Install explicit console capture while preserving the target console's normal output behavior. */
255
311
  export declare function installLogBrewConsoleCapture(config: ConsoleCaptureConfig): ConsoleCaptureHandle;
256
312
 
313
+ /** Create safe action attributes for an app-owned product step without automatic UI capture. */
314
+ export declare function createProductActionAttributes(
315
+ action: ProductActionInput,
316
+ options?: TimelineAttributesOptions
317
+ ): ActionAttributes;
318
+
319
+ /** Create safe action attributes for an app-owned network milestone without HTTP client patching. */
320
+ export declare function createNetworkMilestoneAttributes(
321
+ request: NetworkMilestoneInput,
322
+ options?: TimelineAttributesOptions
323
+ ): ActionAttributes;
324
+
257
325
  /** Convert console arguments into safe LogBrew log attributes without installing capture. */
258
326
  export declare function logAttributesFromConsoleArgs(
259
327
  method: ConsoleMethodName,
@@ -274,6 +342,9 @@ export declare function parseTraceparent(traceparent: string): TraceparentContex
274
342
  /** Create a W3C traceparent value from explicit trace/span ids. */
275
343
  export declare function createTraceparent(input: TraceparentInput): string;
276
344
 
345
+ /** Create an explicit outbound header carrier containing only traceparent. */
346
+ export declare function createTraceparentHeaders(input: TraceparentInput): { traceparent: string };
347
+
277
348
  /** Build LogBrew span attributes that continue an incoming W3C traceparent value. */
278
349
  export declare function spanAttributesFromTraceparent(
279
350
  traceparent: string,
package/index.d.ts CHANGED
@@ -2,6 +2,12 @@
2
2
  export type MetadataValue = string | number | boolean | null;
3
3
  /** Structured metadata map shared by public LogBrew event attribute types. */
4
4
  export type Metadata = Record<string, MetadataValue>;
5
+ /** Canonical user-facing severity categories accepted by LogBrew. */
6
+ export type Severity = "info" | "warning" | "error" | "critical";
7
+ /** Runtime-level aliases accepted for compatibility and normalized before send. */
8
+ export type SeverityAlias = "trace" | "debug" | "warn" | "fatal";
9
+ /** Public severity input accepted by issue and log attributes. */
10
+ export type SeverityInput = Severity | SeverityAlias;
5
11
 
6
12
  /** Parsed W3C trace context from a traceparent value. */
7
13
  export type TraceparentContext = {
@@ -46,7 +52,7 @@ export type EnvironmentAttributes = {
46
52
  /** Public issue event attributes. */
47
53
  export type IssueAttributes = {
48
54
  title: string;
49
- level: "info" | "warning" | "error" | "critical";
55
+ level: SeverityInput;
50
56
  message?: string;
51
57
  metadata?: Metadata;
52
58
  };
@@ -54,7 +60,7 @@ export type IssueAttributes = {
54
60
  /** Public log event attributes. */
55
61
  export type LogAttributes = {
56
62
  message: string;
57
- level: "debug" | "info" | "warning" | "error";
63
+ level: SeverityInput;
58
64
  logger?: string;
59
65
  metadata?: Metadata;
60
66
  };
@@ -177,6 +183,54 @@ export type ActionAttributes = {
177
183
  metadata?: Metadata;
178
184
  };
179
185
 
186
+ /** App-owned product step input for agent-readable action timelines. */
187
+ export type ProductActionInput = string | {
188
+ name: string;
189
+ status?: ActionAttributes["status"];
190
+ sessionId?: string;
191
+ traceId?: string;
192
+ routeTemplate?: string;
193
+ screen?: string;
194
+ funnel?: string;
195
+ step?: string;
196
+ metadata?: Metadata;
197
+ };
198
+
199
+ /** App-owned API milestone input for agent-readable network timelines. */
200
+ export type NetworkMilestoneInput = string | {
201
+ name?: string;
202
+ routeTemplate: string;
203
+ method?: string;
204
+ status?: ActionAttributes["status"];
205
+ statusCode?: number;
206
+ durationMs?: number;
207
+ sessionId?: string;
208
+ traceId?: string;
209
+ metadata?: Metadata;
210
+ };
211
+
212
+ /** Shared timeline helper options for primitive app metadata. */
213
+ export type TimelineAttributesOptions = {
214
+ metadata?: Metadata;
215
+ };
216
+
217
+ /** Public metric event attributes. Use low-cardinality metadata only. */
218
+ export type MetricAttributes = {
219
+ name: string;
220
+ kind: "counter" | "histogram";
221
+ value: number;
222
+ unit: string;
223
+ temporality: "delta" | "cumulative";
224
+ metadata?: Metadata;
225
+ } | {
226
+ name: string;
227
+ kind: "gauge";
228
+ value: number;
229
+ unit: string;
230
+ temporality: "instant";
231
+ metadata?: Metadata;
232
+ };
233
+
180
234
  /** Public event union used in preview and transport payloads. */
181
235
  export type Event =
182
236
  | { type: "release"; id: string; timestamp: string; attributes: ReleaseAttributes }
@@ -184,7 +238,8 @@ export type Event =
184
238
  | { type: "issue"; id: string; timestamp: string; attributes: IssueAttributes }
185
239
  | { type: "log"; id: string; timestamp: string; attributes: LogAttributes }
186
240
  | { type: "span"; id: string; timestamp: string; attributes: SpanAttributes }
187
- | { type: "action"; id: string; timestamp: string; attributes: ActionAttributes };
241
+ | { type: "action"; id: string; timestamp: string; attributes: ActionAttributes }
242
+ | { type: "metric"; id: string; timestamp: string; attributes: MetricAttributes };
188
243
 
189
244
  /** Stable transport response returned from flush and shutdown operations. */
190
245
  export type TransportResponse = {
@@ -245,6 +300,7 @@ export declare class LogBrewClient {
245
300
  log(id: string, timestamp: string, attributes: LogAttributes): void;
246
301
  span(id: string, timestamp: string, attributes: SpanAttributes): void;
247
302
  action(id: string, timestamp: string, attributes: ActionAttributes): void;
303
+ metric(id: string, timestamp: string, attributes: MetricAttributes): void;
248
304
  /** Flush queued events through a transport while preserving retry semantics. */
249
305
  flush(transport: Transport): Promise<TransportResponse>;
250
306
  /** Flush queued events, then mark the client closed so later writes fail. */
@@ -254,6 +310,18 @@ export declare class LogBrewClient {
254
310
  /** Install explicit console capture while preserving the target console's normal output behavior. */
255
311
  export declare function installLogBrewConsoleCapture(config: ConsoleCaptureConfig): ConsoleCaptureHandle;
256
312
 
313
+ /** Create safe action attributes for an app-owned product step without automatic UI capture. */
314
+ export declare function createProductActionAttributes(
315
+ action: ProductActionInput,
316
+ options?: TimelineAttributesOptions
317
+ ): ActionAttributes;
318
+
319
+ /** Create safe action attributes for an app-owned network milestone without HTTP client patching. */
320
+ export declare function createNetworkMilestoneAttributes(
321
+ request: NetworkMilestoneInput,
322
+ options?: TimelineAttributesOptions
323
+ ): ActionAttributes;
324
+
257
325
  /** Convert console arguments into safe LogBrew log attributes without installing capture. */
258
326
  export declare function logAttributesFromConsoleArgs(
259
327
  method: ConsoleMethodName,
@@ -274,6 +342,9 @@ export declare function parseTraceparent(traceparent: string): TraceparentContex
274
342
  /** Create a W3C traceparent value from explicit trace/span ids. */
275
343
  export declare function createTraceparent(input: TraceparentInput): string;
276
344
 
345
+ /** Create an explicit outbound header carrier containing only traceparent. */
346
+ export declare function createTraceparentHeaders(input: TraceparentInput): { traceparent: string };
347
+
277
348
  /** Build LogBrew span attributes that continue an incoming W3C traceparent value. */
278
349
  export declare function spanAttributesFromTraceparent(
279
350
  traceparent: string,
package/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import sdk from "./index.cjs";
2
2
 
3
3
  export const {
4
+ createNetworkMilestoneAttributes,
5
+ createProductActionAttributes,
4
6
  createTraceparent,
7
+ createTraceparentHeaders,
5
8
  createLogBrewPinoDestination,
6
9
  createLogBrewWinstonTransport,
7
10
  installLogBrewConsoleCapture,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",