@logbrew/sdk 0.1.1 → 0.1.3

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
@@ -13,6 +13,14 @@ The package supports both ESM `import` and CommonJS `require`.
13
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.
14
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.
15
15
 
16
+ After install, discover and run the packaged examples:
17
+
18
+ ```bash
19
+ node node_modules/@logbrew/sdk/examples/index.mjs --list
20
+ node node_modules/@logbrew/sdk/examples/index.mjs agent-timeline
21
+ npm --prefix node_modules/@logbrew/sdk/examples run agent-timeline
22
+ ```
23
+
16
24
  ## Example
17
25
 
18
26
  ```js
@@ -120,7 +128,27 @@ await client.flush(RecordingTransport.alwaysAccept());
120
128
 
121
129
  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
130
 
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.
131
+ 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. The shared mapping is documented in the [LogBrew severity contract](../../docs/severity-contract.md).
132
+
133
+ ## Event Filtering
134
+
135
+ Use `eventFilter` when your app needs a last-mile privacy or sampling gate before events enter the in-memory queue. The filter receives a copy of the already validated event, so severity aliases are already canonical and mutations inside the callback do not alter queued payloads. Return `false` to drop an event; return `true` or nothing to keep it.
136
+
137
+ ```js
138
+ const client = LogBrewClient.create({
139
+ apiKey: "LOGBREW_API_KEY",
140
+ sdkName: "checkout-api",
141
+ sdkVersion: "1.0.0",
142
+ eventFilter(event) {
143
+ if (event.type === "log" && event.attributes.level === "info") {
144
+ return false;
145
+ }
146
+ return true;
147
+ }
148
+ });
149
+ ```
150
+
151
+ Prefer removing sensitive values at the source before calling LogBrew. `eventFilter` is intentionally drop-only: it avoids broad mutable event processing, global scopes, and hidden context that can make observability payloads harder to reason about.
124
152
 
125
153
  ## Agent-Readable Timelines
126
154
 
@@ -163,6 +191,13 @@ client.action("evt_payment_api", new Date().toISOString(), createNetworkMileston
163
191
 
164
192
  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`.
165
193
 
194
+ The packaged `agent-timeline` example shows a two-event checkout timeline that an AI assistant can inspect without session replay or payload capture. It combines product action metadata, network milestone metadata, explicit `traceparent` propagation, and a drop-only `eventFilter` that removes low-value info logs:
195
+
196
+ ```bash
197
+ node node_modules/@logbrew/sdk/examples/index.mjs agent-timeline
198
+ node node_modules/@logbrew/sdk/examples/index.mjs agent-timeline:cjs
199
+ ```
200
+
166
201
  ## Console Capture
167
202
 
168
203
  If an app already uses `console.info()`, `console.warn()`, or `console.error()`, install explicit capture on the console object you own:
@@ -0,0 +1,87 @@
1
+ let sdk;
2
+ try {
3
+ sdk = require("@logbrew/sdk");
4
+ } catch (error) {
5
+ if (error && error.code === "MODULE_NOT_FOUND") {
6
+ sdk = require("../index.cjs");
7
+ } else {
8
+ throw error;
9
+ }
10
+ }
11
+
12
+ const {
13
+ createNetworkMilestoneAttributes,
14
+ createProductActionAttributes,
15
+ createTraceparentHeaders,
16
+ LogBrewClient,
17
+ RecordingTransport
18
+ } = sdk;
19
+
20
+ const traceId = "4bf92f3577b34da6a3ce929d0e0e4736";
21
+ const spanId = "b7ad6b7169203331";
22
+ const sessionId = "sess_checkout_001";
23
+ const timestamp = "2026-06-02T10:00:00Z";
24
+
25
+ const client = LogBrewClient.create({
26
+ apiKey: "LOGBREW_API_KEY",
27
+ sdkName: "checkout-agent-timeline",
28
+ sdkVersion: "1.0.0",
29
+ eventFilter(event) {
30
+ return !(event.type === "log" && event.attributes.level === "info");
31
+ }
32
+ });
33
+
34
+ client.action("evt_checkout_started", timestamp, createProductActionAttributes({
35
+ name: "checkout.started",
36
+ status: "success",
37
+ sessionId,
38
+ traceId,
39
+ routeTemplate: "/checkout/:step?coupon=private#payment",
40
+ funnel: "checkout",
41
+ step: "start",
42
+ metadata: { service: "checkout", plan: "pro" }
43
+ }));
44
+
45
+ client.action("evt_payment_api", "2026-06-02T10:00:01Z", createNetworkMilestoneAttributes({
46
+ routeTemplate: "https://api.example.invalid/payments/123?card=private#retry",
47
+ method: "POST",
48
+ statusCode: 503,
49
+ durationMs: 241.5,
50
+ sessionId,
51
+ traceId,
52
+ metadata: { service: "payments", retryable: true }
53
+ }));
54
+
55
+ client.log("evt_debug_noise", "2026-06-02T10:00:02Z", {
56
+ message: "debug heartbeat",
57
+ level: "info",
58
+ logger: "checkout"
59
+ });
60
+
61
+ const headers = createTraceparentHeaders({
62
+ traceId,
63
+ spanId,
64
+ traceFlags: "01"
65
+ });
66
+
67
+ const preview = client.previewJson();
68
+ if (preview.includes("card=private") || preview.includes("coupon=private") || preview.includes("#payment")) {
69
+ throw new Error("agent timeline leaked query or hash metadata");
70
+ }
71
+ if (client.pendingEvents() !== 2) {
72
+ throw new Error(`expected two retained timeline events, got ${client.pendingEvents()}`);
73
+ }
74
+
75
+ console.log(preview);
76
+
77
+ client.shutdown(RecordingTransport.alwaysAccept()).then((response) => {
78
+ console.error(JSON.stringify({
79
+ ok: true,
80
+ events: 2,
81
+ traceparent: headers.traceparent,
82
+ status: response.statusCode
83
+ }));
84
+ }).catch((error) => {
85
+ console.error(error);
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,79 @@
1
+ const sdk = await import("@logbrew/sdk").catch(async (error) => {
2
+ if (error && error.code === "ERR_MODULE_NOT_FOUND") {
3
+ return import("../index.js");
4
+ }
5
+ throw error;
6
+ });
7
+
8
+ const {
9
+ createNetworkMilestoneAttributes,
10
+ createProductActionAttributes,
11
+ createTraceparentHeaders,
12
+ LogBrewClient,
13
+ RecordingTransport
14
+ } = sdk;
15
+
16
+ const traceId = "4bf92f3577b34da6a3ce929d0e0e4736";
17
+ const spanId = "b7ad6b7169203331";
18
+ const sessionId = "sess_checkout_001";
19
+ const timestamp = "2026-06-02T10:00:00Z";
20
+
21
+ const client = LogBrewClient.create({
22
+ apiKey: "LOGBREW_API_KEY",
23
+ sdkName: "checkout-agent-timeline",
24
+ sdkVersion: "1.0.0",
25
+ eventFilter(event) {
26
+ return !(event.type === "log" && event.attributes.level === "info");
27
+ }
28
+ });
29
+
30
+ client.action("evt_checkout_started", timestamp, createProductActionAttributes({
31
+ name: "checkout.started",
32
+ status: "success",
33
+ sessionId,
34
+ traceId,
35
+ routeTemplate: "/checkout/:step?coupon=private#payment",
36
+ funnel: "checkout",
37
+ step: "start",
38
+ metadata: { service: "checkout", plan: "pro" }
39
+ }));
40
+
41
+ client.action("evt_payment_api", "2026-06-02T10:00:01Z", createNetworkMilestoneAttributes({
42
+ routeTemplate: "https://api.example.invalid/payments/123?card=private#retry",
43
+ method: "POST",
44
+ statusCode: 503,
45
+ durationMs: 241.5,
46
+ sessionId,
47
+ traceId,
48
+ metadata: { service: "payments", retryable: true }
49
+ }));
50
+
51
+ client.log("evt_debug_noise", "2026-06-02T10:00:02Z", {
52
+ message: "debug heartbeat",
53
+ level: "info",
54
+ logger: "checkout"
55
+ });
56
+
57
+ const headers = createTraceparentHeaders({
58
+ traceId,
59
+ spanId,
60
+ traceFlags: "01"
61
+ });
62
+
63
+ const preview = client.previewJson();
64
+ if (preview.includes("card=private") || preview.includes("coupon=private") || preview.includes("#payment")) {
65
+ throw new Error("agent timeline leaked query or hash metadata");
66
+ }
67
+ if (client.pendingEvents() !== 2) {
68
+ throw new Error(`expected two retained timeline events, got ${client.pendingEvents()}`);
69
+ }
70
+
71
+ console.log(preview);
72
+
73
+ const response = await client.shutdown(RecordingTransport.alwaysAccept());
74
+ console.error(JSON.stringify({
75
+ ok: true,
76
+ events: 2,
77
+ traceparent: headers.traceparent,
78
+ status: response.statusCode
79
+ }));
@@ -10,6 +10,9 @@ const installedLauncherPrefix = "node node_modules/@logbrew/sdk/examples/index.m
10
10
  const repoLauncherPrefix = "node examples/index.mjs";
11
11
 
12
12
  const examples = {
13
+ "agent-timeline": new URL("./agent-timeline.mjs", import.meta.url),
14
+ "agent-timeline:esm": new URL("./agent-timeline.mjs", import.meta.url),
15
+ "agent-timeline:cjs": new URL("./agent-timeline.cjs", import.meta.url),
13
16
  "readme-example": new URL("./readme-example.mjs", import.meta.url),
14
17
  "readme-example:esm": new URL("./readme-example.mjs", import.meta.url),
15
18
  "readme-example:cjs": new URL("./readme-example.cjs", import.meta.url),
@@ -25,6 +28,9 @@ function isInstalledPackageContext() {
25
28
  function exampleCommands() {
26
29
  if (!isInstalledPackageContext()) {
27
30
  return {
31
+ "agent-timeline": `${repoPrefix} && ${repoLauncherPrefix} agent-timeline`,
32
+ "agent-timeline:esm": `${repoPrefix} && ${repoLauncherPrefix} agent-timeline:esm`,
33
+ "agent-timeline:cjs": `${repoPrefix} && ${repoLauncherPrefix} agent-timeline:cjs`,
28
34
  "readme-example": `${repoPrefix} && ${repoLauncherPrefix} readme-example`,
29
35
  "readme-example:esm": `${repoPrefix} && ${repoLauncherPrefix} readme-example:esm`,
30
36
  "readme-example:cjs": `${repoPrefix} && ${repoLauncherPrefix} readme-example:cjs`,
@@ -36,6 +42,9 @@ function exampleCommands() {
36
42
  }
37
43
 
38
44
  return {
45
+ "agent-timeline": `${installedLauncherPrefix} agent-timeline`,
46
+ "agent-timeline:esm": `${installedLauncherPrefix} agent-timeline:esm`,
47
+ "agent-timeline:cjs": `${installedLauncherPrefix} agent-timeline:cjs`,
39
48
  "readme-example": `${installedLauncherPrefix} readme-example`,
40
49
  "readme-example:esm": `${installedLauncherPrefix} readme-example:esm`,
41
50
  "readme-example:cjs": `${installedLauncherPrefix} readme-example:cjs`,
@@ -49,6 +58,9 @@ function exampleCommands() {
49
58
  function helperCommands() {
50
59
  if (!isInstalledPackageContext()) {
51
60
  return {
61
+ "agent-timeline": `${repoExamplesPrefix} && ${repoNpmHelperPrefix} agent-timeline | ${repoExamplesPrefix} && ${repoPnpmHelperPrefix} agent-timeline`,
62
+ "agent-timeline:esm": `${repoExamplesPrefix} && ${repoNpmHelperPrefix} agent-timeline:esm | ${repoExamplesPrefix} && ${repoPnpmHelperPrefix} agent-timeline:esm`,
63
+ "agent-timeline:cjs": `${repoExamplesPrefix} && ${repoNpmHelperPrefix} agent-timeline:cjs | ${repoExamplesPrefix} && ${repoPnpmHelperPrefix} agent-timeline:cjs`,
52
64
  "readme-example": `${repoExamplesPrefix} && ${repoNpmHelperPrefix} readme-example | ${repoExamplesPrefix} && ${repoPnpmHelperPrefix} readme-example`,
53
65
  "readme-example:esm": `${repoExamplesPrefix} && ${repoNpmHelperPrefix} readme-example:esm | ${repoExamplesPrefix} && ${repoPnpmHelperPrefix} readme-example:esm`,
54
66
  "readme-example:cjs": `${repoExamplesPrefix} && ${repoNpmHelperPrefix} readme-example:cjs | ${repoExamplesPrefix} && ${repoPnpmHelperPrefix} readme-example:cjs`,
@@ -59,6 +71,9 @@ function helperCommands() {
59
71
  }
60
72
 
61
73
  return {
74
+ "agent-timeline": `${installedHelperPrefix} agent-timeline | ${installedPnpmHelperPrefix} agent-timeline`,
75
+ "agent-timeline:esm": `${installedHelperPrefix} agent-timeline:esm | ${installedPnpmHelperPrefix} agent-timeline:esm`,
76
+ "agent-timeline:cjs": `${installedHelperPrefix} agent-timeline:cjs | ${installedPnpmHelperPrefix} agent-timeline:cjs`,
62
77
  "readme-example": `${installedHelperPrefix} readme-example | ${installedPnpmHelperPrefix} readme-example`,
63
78
  "readme-example:esm": `${installedHelperPrefix} readme-example:esm | ${installedPnpmHelperPrefix} readme-example:esm`,
64
79
  "readme-example:cjs": `${installedHelperPrefix} readme-example:cjs | ${installedPnpmHelperPrefix} readme-example:cjs`,
@@ -4,6 +4,9 @@
4
4
  "scripts": {
5
5
  "help": "node ./index.mjs --help",
6
6
  "list": "node ./index.mjs --list",
7
+ "agent-timeline": "node ./index.mjs agent-timeline",
8
+ "agent-timeline:esm": "node ./index.mjs agent-timeline:esm",
9
+ "agent-timeline:cjs": "node ./index.mjs agent-timeline:cjs",
7
10
  "readme-example": "node ./index.mjs readme-example",
8
11
  "readme-example:esm": "node ./index.mjs readme-example:esm",
9
12
  "readme-example:cjs": "node ./index.mjs readme-example:cjs",
package/index.cjs CHANGED
@@ -79,13 +79,17 @@ class RecordingTransport {
79
79
  }
80
80
 
81
81
  class LogBrewClient {
82
- static create({ apiKey, sdkName, sdkVersion, maxRetries = 2 }) {
82
+ static create({ apiKey, sdkName, sdkVersion, maxRetries = 2, eventFilter }) {
83
83
  requireNonEmpty("apiKey", apiKey);
84
84
  requireNonEmpty("sdkName", sdkName);
85
85
  requireNonEmpty("sdkVersion", sdkVersion);
86
+ if (eventFilter !== undefined && typeof eventFilter !== "function") {
87
+ throw new SdkError("validation_error", "eventFilter must be a function");
88
+ }
86
89
 
87
90
  return new LogBrewClient({
88
91
  apiKey,
92
+ eventFilter,
89
93
  sdk: {
90
94
  name: sdkName,
91
95
  language: "javascript",
@@ -95,8 +99,9 @@ class LogBrewClient {
95
99
  });
96
100
  }
97
101
 
98
- constructor({ apiKey, sdk, maxRetries }) {
102
+ constructor({ apiKey, sdk, maxRetries, eventFilter }) {
99
103
  this.apiKey = apiKey;
104
+ this.eventFilter = eventFilter;
100
105
  this.sdk = sdk;
101
106
  this.maxRetries = maxRetries;
102
107
  this.events = [];
@@ -161,7 +166,11 @@ class LogBrewClient {
161
166
  }
162
167
  requireNonEmpty("event id", id);
163
168
  requireTimestamp(timestamp);
164
- this.events.push({ type: eventType, id, timestamp, attributes });
169
+ const event = { type: eventType, id, timestamp, attributes };
170
+ if (this.eventFilter && this.eventFilter(cloneEvent(event)) === false) {
171
+ return;
172
+ }
173
+ this.events.push(event);
165
174
  }
166
175
 
167
176
  async #flushInternal(transport) {
@@ -1004,6 +1013,13 @@ function cloneMetadata(metadata) {
1004
1013
  return { ...metadata };
1005
1014
  }
1006
1015
 
1016
+ function cloneEvent(event) {
1017
+ const attributes = event.attributes.metadata === undefined
1018
+ ? { ...event.attributes }
1019
+ : { ...event.attributes, metadata: { ...event.attributes.metadata } };
1020
+ return { ...event, attributes };
1021
+ }
1022
+
1007
1023
  function validateRelease(attributes) {
1008
1024
  requireNonEmpty("release version", attributes.version);
1009
1025
  if (attributes.commit !== undefined) {
package/index.d.cts CHANGED
@@ -241,6 +241,9 @@ export type Event =
241
241
  | { type: "action"; id: string; timestamp: string; attributes: ActionAttributes }
242
242
  | { type: "metric"; id: string; timestamp: string; attributes: MetricAttributes };
243
243
 
244
+ /** Drop-only event filter called after validation and before an event is queued. */
245
+ export type EventFilter = (event: Event) => boolean | void;
246
+
244
247
  /** Stable transport response returned from flush and shutdown operations. */
245
248
  export type TransportResponse = {
246
249
  /** Final HTTP-like status returned by the transport. */
@@ -289,6 +292,7 @@ export declare class LogBrewClient {
289
292
  sdkName: string;
290
293
  sdkVersion: string;
291
294
  maxRetries?: number;
295
+ eventFilter?: EventFilter;
292
296
  }): LogBrewClient;
293
297
  /** Return the queued event count currently buffered in memory. */
294
298
  pendingEvents(): number;
package/index.d.ts CHANGED
@@ -241,6 +241,9 @@ export type Event =
241
241
  | { type: "action"; id: string; timestamp: string; attributes: ActionAttributes }
242
242
  | { type: "metric"; id: string; timestamp: string; attributes: MetricAttributes };
243
243
 
244
+ /** Drop-only event filter called after validation and before an event is queued. */
245
+ export type EventFilter = (event: Event) => boolean | void;
246
+
244
247
  /** Stable transport response returned from flush and shutdown operations. */
245
248
  export type TransportResponse = {
246
249
  /** Final HTTP-like status returned by the transport. */
@@ -289,6 +292,7 @@ export declare class LogBrewClient {
289
292
  sdkName: string;
290
293
  sdkVersion: string;
291
294
  maxRetries?: number;
295
+ eventFilter?: EventFilter;
292
296
  }): LogBrewClient;
293
297
  /** Return the queued event count currently buffered in memory. */
294
298
  pendingEvents(): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",