@logbrew/sdk 0.1.5 → 0.1.7

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
@@ -134,7 +134,65 @@ try {
134
134
  }
135
135
  ```
136
136
 
137
- The helper records the error name/message and up to 32 ordered generated `stackFrames`, with query strings, hashes, and local absolute prefixes removed. Each frame carries only filename, positive line/column, and an optional matched Debug ID. Existing first-frame metadata remains available for compatible grouping and tooling. The helper also emits an `issueGroupingKey` based on source, error type, and the sanitized first frame, plus an optional app-owned `issueFingerprint` when you pass a stable, safe, low-cardinality `fingerprint`. Nested `Error.cause` chains and `AggregateError.errors` are summarized as bounded cause counts, types, and sources without copying nested messages or stacks. Raw stack text is included only with `includeErrorStack: true`.
137
+ The helper records the error name/message, a typed exception with capture mechanism and handled state, and up to 32 ordered generated `stackFrames`, with query strings, hashes, and local absolute prefixes removed. Each frame carries filename, positive line/column, a conservatively parsed function name when the runtime provides one, and an optional matched Debug ID. Applications that create frames directly may also provide bounded `function`, `module`, and `inApp` identity. Existing first-frame metadata remains available for compatible grouping and tooling. The helper also emits an `issueGroupingKey` based on source, error type, and the sanitized first frame, plus an optional app-owned `issueFingerprint` when you pass a stable, safe, low-cardinality `fingerprint`. Nested `Error.cause` chains and `AggregateError.errors` are summarized as bounded cause counts, types, and sources without copying nested messages or stacks. Raw stack text is included only with `includeErrorStack: true`.
138
+
139
+ Use `addBreadcrumb()` for explicit navigation, state, action, or network steps that should appear on later issues. The client keeps only the most recent 64 entries and marks an issue with `breadcrumbsTruncated: true` after older entries are evicted. `clearBreadcrumbs()` removes the current history.
140
+
141
+ ```js
142
+ client.addBreadcrumb({
143
+ category: "checkout",
144
+ type: "user",
145
+ level: "info",
146
+ message: "Submitted checkout",
147
+ data: { attempt: 2 }
148
+ });
149
+ ```
150
+
151
+ Breadcrumb timestamps default to the current time. Each entry accepts only fixed keys, a bounded message, and at most eight flat primitive data fields. Nested objects, arrays, invalid timestamps, control characters, and oversized values are rejected. Do not include authentication material, user-entered text, raw identifiers, full URLs, query strings, headers, or request/response bodies. The SDK does not capture console output, DOM text, or network payloads to build breadcrumbs.
152
+
153
+ ## Shared Telemetry Context
154
+
155
+ Give issues, logs, spans, actions, metrics, releases, and environments the same bounded identity and correlation data with the versioned `context` option. The JavaScript framework clients accept the same option, so middleware-generated and app-created events share one resource, deployment, session, subject, trace, and tag vocabulary.
156
+
157
+ ```js
158
+ const client = LogBrewClient.create({
159
+ apiKey: "LOGBREW_API_KEY",
160
+ sdkName: "checkout-web",
161
+ sdkVersion: "1.0.0",
162
+ context: {
163
+ schemaVersion: 1,
164
+ resource: {
165
+ service: { name: "checkout-web", version: "1.0.0" },
166
+ deployment: { environment: "production", release: "web@1.0.0" },
167
+ runtime: { name: "browser" },
168
+ framework: { name: "react", version: "19" }
169
+ },
170
+ session: { id: "session_01" },
171
+ subject: { id: "subject_01", kind: "anonymous" },
172
+ tags: { plan: "team", region: "eu" }
173
+ }
174
+ });
175
+
176
+ client.action("evt_checkout_started", new Date().toISOString(), {
177
+ name: "checkout.started",
178
+ status: "success",
179
+ context: {
180
+ schemaVersion: 1,
181
+ trace: {
182
+ traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
183
+ spanId: "b7ad6b7169203331",
184
+ sampled: true
185
+ },
186
+ tags: { funnel: "checkout" }
187
+ }
188
+ });
189
+ ```
190
+
191
+ Client context is copied at creation and merged into every event. Event context can replace the current trace, session, or subject and can add or override individual resource fields and tags. Inputs are validated before queueing: unknown fields, all-zero trace identifiers, empty sections, oversized values, and more than 32 tags are rejected.
192
+
193
+ For span events, the span's required top-level `traceId`, `spanId`, and optional `parentSpanId` remain the canonical span identity. Shared `context.trace` is intended to correlate issues, logs, actions, metrics, releases, and environments that do not already carry a required span identity.
194
+
195
+ `subject.id` and `session.id` are app-owned correlation identifiers, not profile fields. Use opaque identifiers and never put names, email addresses, IP addresses, authentication values, free-form user input, or other personal data in context or tags. The SDK does not discover a user, device, location, session, or product interaction automatically. Capture only the fields your application has deliberately approved, rotate anonymous identifiers according to your privacy policy, and keep tags low-cardinality.
138
196
 
139
197
  ## Example
140
198
 
@@ -519,7 +577,9 @@ client.action("evt_payment_api", new Date().toISOString(), createNetworkMileston
519
577
  }));
520
578
  ```
521
579
 
522
- 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`.
580
+ 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. Product actions also carry the reserved version-1 `interaction` analytics classification; network milestones do not. Keep metadata low-cardinality, such as `sessionId`, `traceId`, `routeTemplate`, `method`, `statusCode`, `durationMs`, `screen`, `funnel`, and `step`.
581
+
582
+ `PRODUCT_ANALYTICS_SCHEMA_VERSION` and `PRODUCT_ANALYTICS_KINDS` expose the classification values supported by the installed package. The reserved analytics fields cannot be replaced through caller metadata. See the repository [product analytics capture contract](../../docs/product-analytics-contract.md) before constructing those fields directly.
523
583
 
524
584
  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:
525
585
 
@@ -585,7 +645,7 @@ logger.error(new Error("payment failed"), "checkout failed");
585
645
  await destination.flush();
586
646
  ```
587
647
 
588
- 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.
648
+ 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 safe primitive Pino fields as `context.*`, captures serialized error name/message, skips noisy runtime defaults, and omits stack text unless `includeErrorStack: true` is set. Credential, authorization, cookie, body, payload, query, raw-URL, propagation-header, local-path, and stack fields are excluded even when they are primitive. It does not patch Pino or replace application logger ownership. The log message itself is telemetry, so retain normal Pino redaction and keep secrets or user-entered text out of messages.
589
649
 
590
650
  When the app also uses a LogBrew Node or framework request helper, pass `traceProvider: getActiveLogBrewTrace` from `@logbrew/node` to add the current active `traceId`, `spanId`, optional `parentSpanId`, and `sampled` flag to each captured log. The provider is called per record, invalid or missing contexts are ignored, and no raw propagation headers, request data, payloads, or stack traces are captured.
591
651
 
package/core.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  const { buildCreateSupportTicketDraft } = require("./support-ticket.cjs");
2
+ const { buildIssueDiagnosticsHelpers } = require("./issue-diagnostics.cjs");
2
3
  const { buildIssueStackHelpers } = require("./issue-stack.cjs");
3
4
  const { buildLogContextHelpers } = require("./log-context.cjs");
4
5
  const { buildOpenTelemetryHelpers } = require("./opentelemetry.cjs");
6
+ const { buildTelemetryContextHelpers } = require("./telemetry-context.cjs");
5
7
  const { buildTraceContextHelpers } = require("./trace-context.cjs");
6
8
 
7
9
  const SEVERITY_ALIASES = new Map([
@@ -17,6 +19,9 @@ const SEVERITY_ALIASES = new Map([
17
19
  const SEVERITY_VALUES = new Set(SEVERITY_ALIASES.keys());
18
20
  const SPAN_STATUSES = new Set(["ok", "error"]);
19
21
  const ACTION_STATUSES = new Set(["queued", "running", "success", "failure"]);
22
+ const PRODUCT_ANALYTICS_SCHEMA_VERSION = 1;
23
+ const PRODUCT_ANALYTICS_KINDS = Object.freeze(["page_view", "screen_view", "interaction"]);
24
+ const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
20
25
  const METRIC_KINDS = new Set(["counter", "gauge", "histogram"]);
21
26
  const NON_NEGATIVE_METRIC_KINDS = new Set(["counter", "histogram"]);
22
27
  const METRIC_TEMPORALITIES_BY_KIND = new Map([
@@ -28,6 +33,44 @@ const CONSOLE_METHODS = new Set(["debug", "info", "log", "warn", "error"]);
28
33
  const DEFAULT_CONSOLE_LEVELS = ["debug", "info", "log", "warn", "error"];
29
34
  const PINO_HOST_FIELD = ["host", "name"].join("");
30
35
  const PINO_RESERVED_FIELDS = new Set(["level", "time", "timestamp", "msg", "message", "err", "error", "pid", PINO_HOST_FIELD, "v"]);
36
+ const PINO_SENSITIVE_CONTEXT_FIELDS = new Set([
37
+ "apikey",
38
+ "authorization",
39
+ "baggage",
40
+ "body",
41
+ "cookie",
42
+ "credentials",
43
+ "cwd",
44
+ "directory",
45
+ "dirname",
46
+ "errorstack",
47
+ "file",
48
+ "filename",
49
+ "filepath",
50
+ "headers",
51
+ "href",
52
+ "key",
53
+ "password",
54
+ "payload",
55
+ "proxyauthorization",
56
+ "query",
57
+ "querystring",
58
+ "requestbody",
59
+ "requestheaders",
60
+ "requesturl",
61
+ "responsebody",
62
+ "responseheaders",
63
+ "responseurl",
64
+ "search",
65
+ "secret",
66
+ "setcookie",
67
+ "stack",
68
+ "token",
69
+ "traceparent",
70
+ "tracestate",
71
+ "uri",
72
+ "url"
73
+ ]);
31
74
  const TRACEPARENT_PATTERN = /^([0-9a-fA-F]{2})-([0-9a-fA-F]{32})-([0-9a-fA-F]{16})-([0-9a-fA-F]{2})$/u;
32
75
  const ZERO_TRACE_ID = "00000000000000000000000000000000";
33
76
  const ZERO_SPAN_ID = "0000000000000000";
@@ -86,6 +129,18 @@ const {
86
129
  } = buildLogContextHelpers({ SdkError });
87
130
 
88
131
  const { javascriptStackFrames, validateIssueStackFrames } = buildIssueStackHelpers({ SdkError });
132
+ const {
133
+ MAX_ISSUE_BREADCRUMBS,
134
+ cloneIssueDiagnostics,
135
+ createIssueException,
136
+ validateIssueBreadcrumb,
137
+ validateIssueDiagnostics
138
+ } = buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp });
139
+ const {
140
+ cloneTelemetryContext,
141
+ mergeTelemetryContexts,
142
+ validateTelemetryContext
143
+ } = buildTelemetryContextHelpers({ SdkError });
89
144
 
90
145
  class TransportError extends Error {
91
146
  constructor(code, message, retryable = false) {
@@ -138,6 +193,7 @@ class LogBrewClient {
138
193
  apiKey,
139
194
  sdkName,
140
195
  sdkVersion,
196
+ context,
141
197
  maxRetries = 2,
142
198
  eventFilter,
143
199
  maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
@@ -155,6 +211,7 @@ class LogBrewClient {
155
211
  requireNonEmpty("apiKey", apiKey);
156
212
  requireNonEmpty("sdkName", sdkName);
157
213
  requireNonEmpty("sdkVersion", sdkVersion);
214
+ const clientContext = validateTelemetryContext(context, "client telemetry context");
158
215
  requireNonNegativeInteger("maxRetries", maxRetries);
159
216
  if (eventFilter !== undefined && typeof eventFilter !== "function") {
160
217
  throw new SdkError("validation_error", "eventFilter must be a function");
@@ -202,6 +259,7 @@ class LogBrewClient {
202
259
  maxQueueBytes,
203
260
  maxQueueSize,
204
261
  onEventDropped,
262
+ context: clientContext,
205
263
  sdk: {
206
264
  name: sdkName,
207
265
  language: "javascript",
@@ -222,6 +280,7 @@ class LogBrewClient {
222
280
  maxQueueBytes,
223
281
  maxQueueSize,
224
282
  onEventDropped,
283
+ context,
225
284
  eventStore,
226
285
  eventQueueFactory,
227
286
  transport,
@@ -239,6 +298,7 @@ class LogBrewClient {
239
298
  this.maxQueueBytes = maxQueueBytes;
240
299
  this.maxQueueSize = maxQueueSize;
241
300
  this.onEventDropped = onEventDropped;
301
+ this.context = cloneTelemetryContext(context);
242
302
  this.transport = transport;
243
303
  this.sdk = sdk;
244
304
  this.maxRetries = maxRetries;
@@ -313,6 +373,8 @@ class LogBrewClient {
313
373
  this.lastAcceptedAtUnixMs = 0;
314
374
  this.lastDroppedAtUnixMs = 0;
315
375
  this.failedBatch = undefined;
376
+ this.issueBreadcrumbs = [];
377
+ this.issueBreadcrumbsTruncated = false;
316
378
  this.#scheduleAutomaticDelivery();
317
379
  }
318
380
 
@@ -416,6 +478,28 @@ class LogBrewClient {
416
478
  return purgedEvents;
417
479
  }
418
480
 
481
+ addBreadcrumb(breadcrumb, timestamp = new Date().toISOString()) {
482
+ if (this.closed) {
483
+ throw new SdkError("shutdown_error", "client is already shut down");
484
+ }
485
+ if (this.closing) {
486
+ throw new SdkError("shutdown_error", "client is shutting down");
487
+ }
488
+ const validated = validateIssueBreadcrumb(breadcrumb, timestamp);
489
+ if (this.issueBreadcrumbs.length === MAX_ISSUE_BREADCRUMBS) {
490
+ this.issueBreadcrumbs.shift();
491
+ this.issueBreadcrumbsTruncated = true;
492
+ }
493
+ this.issueBreadcrumbs.push(validated);
494
+ }
495
+
496
+ clearBreadcrumbs() {
497
+ const cleared = this.issueBreadcrumbs.length;
498
+ this.issueBreadcrumbs.splice(0, cleared);
499
+ this.issueBreadcrumbsTruncated = false;
500
+ return cleared;
501
+ }
502
+
419
503
  release(id, timestamp, attributes) {
420
504
  this.#pushEvent("release", id, timestamp, validateRelease(attributes));
421
505
  }
@@ -425,7 +509,18 @@ class LogBrewClient {
425
509
  }
426
510
 
427
511
  issue(id, timestamp, attributes) {
428
- this.#pushEvent("issue", id, timestamp, validateIssue(attributes));
512
+ const withBreadcrumbs = attributes.breadcrumbs !== undefined || this.issueBreadcrumbs.length === 0
513
+ ? attributes
514
+ : {
515
+ ...attributes,
516
+ breadcrumbs: this.issueBreadcrumbs,
517
+ ...(
518
+ attributes.breadcrumbsTruncated === true || this.issueBreadcrumbsTruncated
519
+ ? { breadcrumbsTruncated: true }
520
+ : {}
521
+ )
522
+ };
523
+ this.#pushEvent("issue", id, timestamp, validateIssue(withBreadcrumbs));
429
524
  }
430
525
 
431
526
  log(id, timestamp, attributes) {
@@ -523,7 +618,11 @@ class LogBrewClient {
523
618
  }
524
619
  requireNonEmpty("event id", id);
525
620
  requireTimestamp(timestamp);
526
- const event = { type: eventType, id, timestamp, attributes };
621
+ const context = mergeTelemetryContexts(this.context, attributes.context);
622
+ const eventAttributes = context === undefined
623
+ ? attributes
624
+ : { ...attributes, context };
625
+ const event = { type: eventType, id, timestamp, attributes: eventAttributes };
527
626
  if (this.eventFilter && this.eventFilter(cloneEvent(event)) === false) {
528
627
  return;
529
628
  }
@@ -1292,6 +1391,11 @@ function createIssueAttributesFromError(error, options = {}) {
1292
1391
  title: stringOrUndefined(options.title) ?? details.name,
1293
1392
  level: normalizeSeverity("issue level", options.level ?? "error"),
1294
1393
  ...(stringOrUndefined(options.message) ? { message: options.message } : details.message ? { message: details.message } : {}),
1394
+ exception: createIssueException(
1395
+ boundedIssueExceptionType(details.name),
1396
+ stringOrUndefined(options.mechanism) ?? "javascript.error",
1397
+ options.handled === undefined ? true : options.handled
1398
+ ),
1295
1399
  ...(stackFrames.length > 0 ? { stackFrames } : {}),
1296
1400
  metadata: compactMetadata(metadata)
1297
1401
  };
@@ -1317,6 +1421,23 @@ function errorDetails(error) {
1317
1421
  return { name: "Error" };
1318
1422
  }
1319
1423
 
1424
+ function boundedIssueExceptionType(value) {
1425
+ const normalized = typeof value === "string" ? value.trim() : "";
1426
+ const characters = Array.from(normalized);
1427
+ if (
1428
+ normalized === ""
1429
+ || characters.length > 256
1430
+ || /[?#]/u.test(normalized)
1431
+ || characters.some((character) => {
1432
+ const code = character.codePointAt(0);
1433
+ return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
1434
+ })
1435
+ ) {
1436
+ return "Error";
1437
+ }
1438
+ return normalized;
1439
+ }
1440
+
1320
1441
  function issueGroupingMetadata(source, details, frame, fingerprint) {
1321
1442
  const groupingKey = frame
1322
1443
  ? `${source}:${details.name}:${frame.filename}`
@@ -1467,6 +1588,7 @@ function logbrewLevelFromConsoleMethod(method) {
1467
1588
 
1468
1589
  function createProductActionAttributes(action, options = {}) {
1469
1590
  const details = productActionDetails(action);
1591
+ const routeTemplate = sanitizeRouteTemplate(details.routeTemplate);
1470
1592
  return {
1471
1593
  name: details.name,
1472
1594
  status: details.status,
@@ -1474,12 +1596,13 @@ function createProductActionAttributes(action, options = {}) {
1474
1596
  source: "product.action",
1475
1597
  ...compactMetadata(options.metadata),
1476
1598
  ...compactMetadata(details.metadata),
1477
- routeTemplate: sanitizeRouteTemplate(details.routeTemplate),
1599
+ routeTemplate,
1478
1600
  sessionId: stringOrUndefined(details.sessionId),
1479
1601
  traceId: stringOrUndefined(details.traceId),
1480
1602
  screen: stringOrUndefined(details.screen),
1481
1603
  funnel: stringOrUndefined(details.funnel),
1482
- step: stringOrUndefined(details.step)
1604
+ step: stringOrUndefined(details.step),
1605
+ ...productAnalyticsMetadata("interaction", routeTemplate || details.screen)
1483
1606
  })
1484
1607
  };
1485
1608
  }
@@ -1798,13 +1921,35 @@ function pinoMessage(record) {
1798
1921
  function pinoContextMetadata(record) {
1799
1922
  const metadata = {};
1800
1923
  for (const [key, value] of Object.entries(record)) {
1801
- if (!PINO_RESERVED_FIELDS.has(key) && isMetadataValue(value)) {
1924
+ if (!PINO_RESERVED_FIELDS.has(key) && !isSensitivePinoContextField(key) && isMetadataValue(value)) {
1802
1925
  metadata[`context.${key}`] = value;
1803
1926
  }
1804
1927
  }
1805
1928
  return metadata;
1806
1929
  }
1807
1930
 
1931
+ function isSensitivePinoContextField(key) {
1932
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]+/gu, "");
1933
+ return (
1934
+ PINO_SENSITIVE_CONTEXT_FIELDS.has(normalized)
1935
+ || normalized.endsWith("accesskey")
1936
+ || normalized.endsWith("apikey")
1937
+ || normalized.endsWith("authorization")
1938
+ || normalized.endsWith("body")
1939
+ || normalized.endsWith("cookie")
1940
+ || normalized.endsWith("credentials")
1941
+ || normalized.endsWith("headers")
1942
+ || normalized.endsWith("password")
1943
+ || normalized.endsWith("payload")
1944
+ || normalized.endsWith("privatekey")
1945
+ || normalized.endsWith("query")
1946
+ || normalized.endsWith("secret")
1947
+ || normalized.endsWith("stack")
1948
+ || normalized.endsWith("token")
1949
+ || normalized.endsWith("url")
1950
+ );
1951
+ }
1952
+
1808
1953
  function addPinoErrorMetadata(metadata, error, includeErrorStack) {
1809
1954
  if (!error) {
1810
1955
  return;
@@ -1942,7 +2087,7 @@ function cloneSpanLinks(links) {
1942
2087
  }
1943
2088
 
1944
2089
  function cloneEvent(event) {
1945
- const attributes = { ...event.attributes };
2090
+ const attributes = { ...event.attributes, ...cloneIssueDiagnostics(event.attributes) };
1946
2091
  if (event.attributes.metadata !== undefined) {
1947
2092
  attributes.metadata = { ...event.attributes.metadata };
1948
2093
  }
@@ -1952,6 +2097,9 @@ function cloneEvent(event) {
1952
2097
  if (Array.isArray(event.attributes.links)) {
1953
2098
  attributes.links = cloneSpanLinks(event.attributes.links);
1954
2099
  }
2100
+ if (event.attributes.context !== undefined) {
2101
+ attributes.context = cloneTelemetryContext(event.attributes.context);
2102
+ }
1955
2103
  return { ...event, attributes };
1956
2104
  }
1957
2105
 
@@ -1964,7 +2112,7 @@ function validateRelease(attributes) {
1964
2112
  version: attributes.version,
1965
2113
  ...(attributes.commit ? { commit: attributes.commit } : {}),
1966
2114
  ...(attributes.notes !== undefined ? { notes: attributes.notes } : {})
1967
- }, attributes.metadata);
2115
+ }, attributes.metadata, attributes.context);
1968
2116
  }
1969
2117
 
1970
2118
  function validateEnvironment(attributes) {
@@ -1972,7 +2120,7 @@ function validateEnvironment(attributes) {
1972
2120
  return withMetadata({
1973
2121
  name: attributes.name,
1974
2122
  ...(attributes.region !== undefined ? { region: attributes.region } : {})
1975
- }, attributes.metadata);
2123
+ }, attributes.metadata, attributes.context);
1976
2124
  }
1977
2125
 
1978
2126
  function validateIssue(attributes) {
@@ -1983,8 +2131,9 @@ function validateIssue(attributes) {
1983
2131
  title: attributes.title,
1984
2132
  level,
1985
2133
  ...(attributes.message !== undefined ? { message: attributes.message } : {}),
1986
- ...(stackFrames !== undefined ? { stackFrames } : {})
1987
- }, attributes.metadata);
2134
+ ...(stackFrames !== undefined ? { stackFrames } : {}),
2135
+ ...validateIssueDiagnostics(attributes)
2136
+ }, attributes.metadata, attributes.context);
1988
2137
  }
1989
2138
 
1990
2139
  function validateLog(attributes) {
@@ -1994,7 +2143,7 @@ function validateLog(attributes) {
1994
2143
  message: attributes.message,
1995
2144
  level,
1996
2145
  ...(attributes.logger !== undefined ? { logger: attributes.logger } : {})
1997
- }, attributes.metadata);
2146
+ }, attributes.metadata, attributes.context);
1998
2147
  }
1999
2148
 
2000
2149
  function normalizeSeverity(label, value) {
@@ -2026,7 +2175,7 @@ function validateSpan(attributes) {
2026
2175
  ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
2027
2176
  ...(events !== undefined ? { events } : {}),
2028
2177
  ...(links !== undefined ? { links } : {})
2029
- }, attributes.metadata);
2178
+ }, attributes.metadata, attributes.context);
2030
2179
  }
2031
2180
 
2032
2181
  function validateSpanEvents(events) {
@@ -2109,7 +2258,7 @@ function validateAction(attributes) {
2109
2258
  return withMetadata({
2110
2259
  name: attributes.name,
2111
2260
  status: attributes.status
2112
- }, attributes.metadata);
2261
+ }, attributes.metadata, attributes.context);
2113
2262
  }
2114
2263
 
2115
2264
  function validateMetric(attributes) {
@@ -2130,7 +2279,7 @@ function validateMetric(attributes) {
2130
2279
  value: attributes.value,
2131
2280
  unit: attributes.unit,
2132
2281
  temporality: attributes.temporality
2133
- }, attributes.metadata);
2282
+ }, attributes.metadata, attributes.context);
2134
2283
  }
2135
2284
 
2136
2285
  function productActionDetails(action) {
@@ -2209,6 +2358,34 @@ function sanitizeRouteTemplate(routeTemplate) {
2209
2358
  }
2210
2359
  }
2211
2360
 
2361
+ function productAnalyticsMetadata(kind, surface) {
2362
+ const normalizedSurface = boundedProductAnalyticsSurface(surface);
2363
+ return {
2364
+ analyticsSchemaVersion: PRODUCT_ANALYTICS_SCHEMA_VERSION,
2365
+ analyticsKind: kind,
2366
+ analyticsSurface: normalizedSurface
2367
+ };
2368
+ }
2369
+
2370
+ function boundedProductAnalyticsSurface(surface) {
2371
+ if (typeof surface !== "string") {
2372
+ return undefined;
2373
+ }
2374
+ const normalized = surface.trim();
2375
+ const characters = Array.from(normalized);
2376
+ if (
2377
+ normalized === ""
2378
+ || characters.length > MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH
2379
+ || characters.some((character) => {
2380
+ const code = character.codePointAt(0);
2381
+ return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
2382
+ })
2383
+ ) {
2384
+ return undefined;
2385
+ }
2386
+ return normalized;
2387
+ }
2388
+
2212
2389
  function normalizeHttpMethod(method) {
2213
2390
  const value = method === undefined ? "GET" : method;
2214
2391
  if (typeof value !== "string" || value.trim() === "") {
@@ -2252,11 +2429,14 @@ function stringOrUndefined(value) {
2252
2429
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
2253
2430
  }
2254
2431
 
2255
- function withMetadata(attributes, metadata) {
2432
+ function withMetadata(attributes, metadata, context) {
2256
2433
  const safeMetadata = cloneMetadata(metadata);
2257
- return safeMetadata === undefined
2258
- ? attributes
2259
- : { ...attributes, metadata: safeMetadata };
2434
+ const safeContext = validateTelemetryContext(context);
2435
+ return {
2436
+ ...attributes,
2437
+ ...(safeMetadata === undefined ? {} : { metadata: safeMetadata }),
2438
+ ...(safeContext === undefined ? {} : { context: safeContext })
2439
+ };
2260
2440
  }
2261
2441
 
2262
2442
  function normalizeConsoleLevels(levels) {
@@ -2307,6 +2487,8 @@ function formatConsoleArgument(value, includeErrorStack) {
2307
2487
  }
2308
2488
 
2309
2489
  module.exports = {
2490
+ PRODUCT_ANALYTICS_KINDS,
2491
+ PRODUCT_ANALYTICS_SCHEMA_VERSION,
2310
2492
  createBaggage,
2311
2493
  createIssueAttributesFromError,
2312
2494
  createNetworkMilestoneAttributes,