@logbrew/sdk 0.1.6 → 0.1.8

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
 
@@ -192,6 +250,7 @@ Use `client.metric()` when application code already knows the measurement name,
192
250
  ```js
193
251
  client.metric("evt_metric_001", "2026-06-02T10:00:06Z", {
194
252
  name: "checkout.requests",
253
+ description: "Number of checkout requests accepted by the application.",
195
254
  kind: "counter",
196
255
  value: 42,
197
256
  unit: "{request}",
@@ -200,7 +259,7 @@ client.metric("evt_metric_001", "2026-06-02T10:00:06Z", {
200
259
  });
201
260
  ```
202
261
 
203
- 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.
262
+ 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. An optional `description` gives humans and investigation tools stable meaning for the measurement; keep it generic, single-line, between 1 and 1,024 Unicode characters, and free of identifiers, personal data, or changing values. It is not a query dimension. Keep metric metadata primitive and low-cardinality, such as service, region, or route template.
204
263
 
205
264
  ## W3C Trace Context
206
265
 
@@ -519,7 +578,9 @@ client.action("evt_payment_api", new Date().toISOString(), createNetworkMileston
519
578
  }));
520
579
  ```
521
580
 
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`.
581
+ 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`.
582
+
583
+ `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
584
 
524
585
  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
586
 
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,7 +19,11 @@ 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"]);
26
+ const MAX_METRIC_DESCRIPTION_LENGTH = 1024;
21
27
  const NON_NEGATIVE_METRIC_KINDS = new Set(["counter", "histogram"]);
22
28
  const METRIC_TEMPORALITIES_BY_KIND = new Map([
23
29
  ["counter", new Set(["delta", "cumulative"])],
@@ -124,6 +130,18 @@ const {
124
130
  } = buildLogContextHelpers({ SdkError });
125
131
 
126
132
  const { javascriptStackFrames, validateIssueStackFrames } = buildIssueStackHelpers({ SdkError });
133
+ const {
134
+ MAX_ISSUE_BREADCRUMBS,
135
+ cloneIssueDiagnostics,
136
+ createIssueException,
137
+ validateIssueBreadcrumb,
138
+ validateIssueDiagnostics
139
+ } = buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp });
140
+ const {
141
+ cloneTelemetryContext,
142
+ mergeTelemetryContexts,
143
+ validateTelemetryContext
144
+ } = buildTelemetryContextHelpers({ SdkError });
127
145
 
128
146
  class TransportError extends Error {
129
147
  constructor(code, message, retryable = false) {
@@ -176,6 +194,7 @@ class LogBrewClient {
176
194
  apiKey,
177
195
  sdkName,
178
196
  sdkVersion,
197
+ context,
179
198
  maxRetries = 2,
180
199
  eventFilter,
181
200
  maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
@@ -193,6 +212,7 @@ class LogBrewClient {
193
212
  requireNonEmpty("apiKey", apiKey);
194
213
  requireNonEmpty("sdkName", sdkName);
195
214
  requireNonEmpty("sdkVersion", sdkVersion);
215
+ const clientContext = validateTelemetryContext(context, "client telemetry context");
196
216
  requireNonNegativeInteger("maxRetries", maxRetries);
197
217
  if (eventFilter !== undefined && typeof eventFilter !== "function") {
198
218
  throw new SdkError("validation_error", "eventFilter must be a function");
@@ -240,6 +260,7 @@ class LogBrewClient {
240
260
  maxQueueBytes,
241
261
  maxQueueSize,
242
262
  onEventDropped,
263
+ context: clientContext,
243
264
  sdk: {
244
265
  name: sdkName,
245
266
  language: "javascript",
@@ -260,6 +281,7 @@ class LogBrewClient {
260
281
  maxQueueBytes,
261
282
  maxQueueSize,
262
283
  onEventDropped,
284
+ context,
263
285
  eventStore,
264
286
  eventQueueFactory,
265
287
  transport,
@@ -277,6 +299,7 @@ class LogBrewClient {
277
299
  this.maxQueueBytes = maxQueueBytes;
278
300
  this.maxQueueSize = maxQueueSize;
279
301
  this.onEventDropped = onEventDropped;
302
+ this.context = cloneTelemetryContext(context);
280
303
  this.transport = transport;
281
304
  this.sdk = sdk;
282
305
  this.maxRetries = maxRetries;
@@ -351,6 +374,8 @@ class LogBrewClient {
351
374
  this.lastAcceptedAtUnixMs = 0;
352
375
  this.lastDroppedAtUnixMs = 0;
353
376
  this.failedBatch = undefined;
377
+ this.issueBreadcrumbs = [];
378
+ this.issueBreadcrumbsTruncated = false;
354
379
  this.#scheduleAutomaticDelivery();
355
380
  }
356
381
 
@@ -454,6 +479,28 @@ class LogBrewClient {
454
479
  return purgedEvents;
455
480
  }
456
481
 
482
+ addBreadcrumb(breadcrumb, timestamp = new Date().toISOString()) {
483
+ if (this.closed) {
484
+ throw new SdkError("shutdown_error", "client is already shut down");
485
+ }
486
+ if (this.closing) {
487
+ throw new SdkError("shutdown_error", "client is shutting down");
488
+ }
489
+ const validated = validateIssueBreadcrumb(breadcrumb, timestamp);
490
+ if (this.issueBreadcrumbs.length === MAX_ISSUE_BREADCRUMBS) {
491
+ this.issueBreadcrumbs.shift();
492
+ this.issueBreadcrumbsTruncated = true;
493
+ }
494
+ this.issueBreadcrumbs.push(validated);
495
+ }
496
+
497
+ clearBreadcrumbs() {
498
+ const cleared = this.issueBreadcrumbs.length;
499
+ this.issueBreadcrumbs.splice(0, cleared);
500
+ this.issueBreadcrumbsTruncated = false;
501
+ return cleared;
502
+ }
503
+
457
504
  release(id, timestamp, attributes) {
458
505
  this.#pushEvent("release", id, timestamp, validateRelease(attributes));
459
506
  }
@@ -463,7 +510,18 @@ class LogBrewClient {
463
510
  }
464
511
 
465
512
  issue(id, timestamp, attributes) {
466
- this.#pushEvent("issue", id, timestamp, validateIssue(attributes));
513
+ const withBreadcrumbs = attributes.breadcrumbs !== undefined || this.issueBreadcrumbs.length === 0
514
+ ? attributes
515
+ : {
516
+ ...attributes,
517
+ breadcrumbs: this.issueBreadcrumbs,
518
+ ...(
519
+ attributes.breadcrumbsTruncated === true || this.issueBreadcrumbsTruncated
520
+ ? { breadcrumbsTruncated: true }
521
+ : {}
522
+ )
523
+ };
524
+ this.#pushEvent("issue", id, timestamp, validateIssue(withBreadcrumbs));
467
525
  }
468
526
 
469
527
  log(id, timestamp, attributes) {
@@ -561,7 +619,11 @@ class LogBrewClient {
561
619
  }
562
620
  requireNonEmpty("event id", id);
563
621
  requireTimestamp(timestamp);
564
- const event = { type: eventType, id, timestamp, attributes };
622
+ const context = mergeTelemetryContexts(this.context, attributes.context);
623
+ const eventAttributes = context === undefined
624
+ ? attributes
625
+ : { ...attributes, context };
626
+ const event = { type: eventType, id, timestamp, attributes: eventAttributes };
565
627
  if (this.eventFilter && this.eventFilter(cloneEvent(event)) === false) {
566
628
  return;
567
629
  }
@@ -1330,6 +1392,11 @@ function createIssueAttributesFromError(error, options = {}) {
1330
1392
  title: stringOrUndefined(options.title) ?? details.name,
1331
1393
  level: normalizeSeverity("issue level", options.level ?? "error"),
1332
1394
  ...(stringOrUndefined(options.message) ? { message: options.message } : details.message ? { message: details.message } : {}),
1395
+ exception: createIssueException(
1396
+ boundedIssueExceptionType(details.name),
1397
+ stringOrUndefined(options.mechanism) ?? "javascript.error",
1398
+ options.handled === undefined ? true : options.handled
1399
+ ),
1333
1400
  ...(stackFrames.length > 0 ? { stackFrames } : {}),
1334
1401
  metadata: compactMetadata(metadata)
1335
1402
  };
@@ -1355,6 +1422,23 @@ function errorDetails(error) {
1355
1422
  return { name: "Error" };
1356
1423
  }
1357
1424
 
1425
+ function boundedIssueExceptionType(value) {
1426
+ const normalized = typeof value === "string" ? value.trim() : "";
1427
+ const characters = Array.from(normalized);
1428
+ if (
1429
+ normalized === ""
1430
+ || characters.length > 256
1431
+ || /[?#]/u.test(normalized)
1432
+ || characters.some((character) => {
1433
+ const code = character.codePointAt(0);
1434
+ return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
1435
+ })
1436
+ ) {
1437
+ return "Error";
1438
+ }
1439
+ return normalized;
1440
+ }
1441
+
1358
1442
  function issueGroupingMetadata(source, details, frame, fingerprint) {
1359
1443
  const groupingKey = frame
1360
1444
  ? `${source}:${details.name}:${frame.filename}`
@@ -1505,6 +1589,7 @@ function logbrewLevelFromConsoleMethod(method) {
1505
1589
 
1506
1590
  function createProductActionAttributes(action, options = {}) {
1507
1591
  const details = productActionDetails(action);
1592
+ const routeTemplate = sanitizeRouteTemplate(details.routeTemplate);
1508
1593
  return {
1509
1594
  name: details.name,
1510
1595
  status: details.status,
@@ -1512,12 +1597,13 @@ function createProductActionAttributes(action, options = {}) {
1512
1597
  source: "product.action",
1513
1598
  ...compactMetadata(options.metadata),
1514
1599
  ...compactMetadata(details.metadata),
1515
- routeTemplate: sanitizeRouteTemplate(details.routeTemplate),
1600
+ routeTemplate,
1516
1601
  sessionId: stringOrUndefined(details.sessionId),
1517
1602
  traceId: stringOrUndefined(details.traceId),
1518
1603
  screen: stringOrUndefined(details.screen),
1519
1604
  funnel: stringOrUndefined(details.funnel),
1520
- step: stringOrUndefined(details.step)
1605
+ step: stringOrUndefined(details.step),
1606
+ ...productAnalyticsMetadata("interaction", routeTemplate || details.screen)
1521
1607
  })
1522
1608
  };
1523
1609
  }
@@ -2002,7 +2088,7 @@ function cloneSpanLinks(links) {
2002
2088
  }
2003
2089
 
2004
2090
  function cloneEvent(event) {
2005
- const attributes = { ...event.attributes };
2091
+ const attributes = { ...event.attributes, ...cloneIssueDiagnostics(event.attributes) };
2006
2092
  if (event.attributes.metadata !== undefined) {
2007
2093
  attributes.metadata = { ...event.attributes.metadata };
2008
2094
  }
@@ -2012,6 +2098,9 @@ function cloneEvent(event) {
2012
2098
  if (Array.isArray(event.attributes.links)) {
2013
2099
  attributes.links = cloneSpanLinks(event.attributes.links);
2014
2100
  }
2101
+ if (event.attributes.context !== undefined) {
2102
+ attributes.context = cloneTelemetryContext(event.attributes.context);
2103
+ }
2015
2104
  return { ...event, attributes };
2016
2105
  }
2017
2106
 
@@ -2024,7 +2113,7 @@ function validateRelease(attributes) {
2024
2113
  version: attributes.version,
2025
2114
  ...(attributes.commit ? { commit: attributes.commit } : {}),
2026
2115
  ...(attributes.notes !== undefined ? { notes: attributes.notes } : {})
2027
- }, attributes.metadata);
2116
+ }, attributes.metadata, attributes.context);
2028
2117
  }
2029
2118
 
2030
2119
  function validateEnvironment(attributes) {
@@ -2032,7 +2121,7 @@ function validateEnvironment(attributes) {
2032
2121
  return withMetadata({
2033
2122
  name: attributes.name,
2034
2123
  ...(attributes.region !== undefined ? { region: attributes.region } : {})
2035
- }, attributes.metadata);
2124
+ }, attributes.metadata, attributes.context);
2036
2125
  }
2037
2126
 
2038
2127
  function validateIssue(attributes) {
@@ -2043,8 +2132,9 @@ function validateIssue(attributes) {
2043
2132
  title: attributes.title,
2044
2133
  level,
2045
2134
  ...(attributes.message !== undefined ? { message: attributes.message } : {}),
2046
- ...(stackFrames !== undefined ? { stackFrames } : {})
2047
- }, attributes.metadata);
2135
+ ...(stackFrames !== undefined ? { stackFrames } : {}),
2136
+ ...validateIssueDiagnostics(attributes)
2137
+ }, attributes.metadata, attributes.context);
2048
2138
  }
2049
2139
 
2050
2140
  function validateLog(attributes) {
@@ -2054,7 +2144,7 @@ function validateLog(attributes) {
2054
2144
  message: attributes.message,
2055
2145
  level,
2056
2146
  ...(attributes.logger !== undefined ? { logger: attributes.logger } : {})
2057
- }, attributes.metadata);
2147
+ }, attributes.metadata, attributes.context);
2058
2148
  }
2059
2149
 
2060
2150
  function normalizeSeverity(label, value) {
@@ -2086,7 +2176,7 @@ function validateSpan(attributes) {
2086
2176
  ...(attributes.durationMs !== undefined ? { durationMs: attributes.durationMs } : {}),
2087
2177
  ...(events !== undefined ? { events } : {}),
2088
2178
  ...(links !== undefined ? { links } : {})
2089
- }, attributes.metadata);
2179
+ }, attributes.metadata, attributes.context);
2090
2180
  }
2091
2181
 
2092
2182
  function validateSpanEvents(events) {
@@ -2169,7 +2259,7 @@ function validateAction(attributes) {
2169
2259
  return withMetadata({
2170
2260
  name: attributes.name,
2171
2261
  status: attributes.status
2172
- }, attributes.metadata);
2262
+ }, attributes.metadata, attributes.context);
2173
2263
  }
2174
2264
 
2175
2265
  function validateMetric(attributes) {
@@ -2183,14 +2273,43 @@ function validateMetric(attributes) {
2183
2273
  if (NON_NEGATIVE_METRIC_KINDS.has(attributes.kind) && attributes.value < 0) {
2184
2274
  throw new SdkError("validation_error", `metric ${attributes.kind} value must be non-negative`);
2185
2275
  }
2276
+ const description = metricDescriptionOrUndefined(attributes.description);
2186
2277
 
2187
2278
  return withMetadata({
2188
2279
  name: attributes.name,
2280
+ ...(description === undefined ? {} : { description }),
2189
2281
  kind: attributes.kind,
2190
2282
  value: attributes.value,
2191
2283
  unit: attributes.unit,
2192
2284
  temporality: attributes.temporality
2193
- }, attributes.metadata);
2285
+ }, attributes.metadata, attributes.context);
2286
+ }
2287
+
2288
+ function metricDescriptionOrUndefined(value) {
2289
+ if (value === undefined) {
2290
+ return undefined;
2291
+ }
2292
+ const characters = typeof value === "string" ? Array.from(value) : [];
2293
+ if (
2294
+ typeof value !== "string"
2295
+ || value.trim() === ""
2296
+ || characters.length > MAX_METRIC_DESCRIPTION_LENGTH
2297
+ || characters.some((character) => {
2298
+ const code = character.codePointAt(0);
2299
+ return code !== undefined && (
2300
+ code <= 31
2301
+ || (code >= 127 && code <= 159)
2302
+ || code === 0x2028
2303
+ || code === 0x2029
2304
+ );
2305
+ })
2306
+ ) {
2307
+ throw new SdkError(
2308
+ "validation_error",
2309
+ `metric description must be a non-blank string of at most ${MAX_METRIC_DESCRIPTION_LENGTH} non-control characters`
2310
+ );
2311
+ }
2312
+ return value.trim();
2194
2313
  }
2195
2314
 
2196
2315
  function productActionDetails(action) {
@@ -2269,6 +2388,34 @@ function sanitizeRouteTemplate(routeTemplate) {
2269
2388
  }
2270
2389
  }
2271
2390
 
2391
+ function productAnalyticsMetadata(kind, surface) {
2392
+ const normalizedSurface = boundedProductAnalyticsSurface(surface);
2393
+ return {
2394
+ analyticsSchemaVersion: PRODUCT_ANALYTICS_SCHEMA_VERSION,
2395
+ analyticsKind: kind,
2396
+ analyticsSurface: normalizedSurface
2397
+ };
2398
+ }
2399
+
2400
+ function boundedProductAnalyticsSurface(surface) {
2401
+ if (typeof surface !== "string") {
2402
+ return undefined;
2403
+ }
2404
+ const normalized = surface.trim();
2405
+ const characters = Array.from(normalized);
2406
+ if (
2407
+ normalized === ""
2408
+ || characters.length > MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH
2409
+ || characters.some((character) => {
2410
+ const code = character.codePointAt(0);
2411
+ return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
2412
+ })
2413
+ ) {
2414
+ return undefined;
2415
+ }
2416
+ return normalized;
2417
+ }
2418
+
2272
2419
  function normalizeHttpMethod(method) {
2273
2420
  const value = method === undefined ? "GET" : method;
2274
2421
  if (typeof value !== "string" || value.trim() === "") {
@@ -2312,11 +2459,14 @@ function stringOrUndefined(value) {
2312
2459
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
2313
2460
  }
2314
2461
 
2315
- function withMetadata(attributes, metadata) {
2462
+ function withMetadata(attributes, metadata, context) {
2316
2463
  const safeMetadata = cloneMetadata(metadata);
2317
- return safeMetadata === undefined
2318
- ? attributes
2319
- : { ...attributes, metadata: safeMetadata };
2464
+ const safeContext = validateTelemetryContext(context);
2465
+ return {
2466
+ ...attributes,
2467
+ ...(safeMetadata === undefined ? {} : { metadata: safeMetadata }),
2468
+ ...(safeContext === undefined ? {} : { context: safeContext })
2469
+ };
2320
2470
  }
2321
2471
 
2322
2472
  function normalizeConsoleLevels(levels) {
@@ -2367,6 +2517,8 @@ function formatConsoleArgument(value, includeErrorStack) {
2367
2517
  }
2368
2518
 
2369
2519
  module.exports = {
2520
+ PRODUCT_ANALYTICS_KINDS,
2521
+ PRODUCT_ANALYTICS_SCHEMA_VERSION,
2370
2522
  createBaggage,
2371
2523
  createIssueAttributesFromError,
2372
2524
  createNetworkMilestoneAttributes,