@juspay/neurolink 9.25.0 → 9.25.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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [9.25.1](https://github.com/juspay/neurolink/compare/v9.25.0...v9.25.1) (2026-03-15)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(observability):** address code review findings from PR [#860](https://github.com/juspay/neurolink/issues/860) ([1655c7e](https://github.com/juspay/neurolink/commit/1655c7ef6991eb104834e10fa7d21516539ea16d))
6
+
1
7
  ## [9.25.0](https://github.com/juspay/neurolink/compare/v9.24.0...v9.25.0) (2026-03-15)
2
8
 
3
9
  ### Features
@@ -3,13 +3,9 @@ import { generateText } from "ai";
3
3
  import { directAgentTools } from "../agent/directTools.js";
4
4
  import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
5
5
  import { MiddlewareFactory } from "../middleware/factory.js";
6
- import { getMetricsAggregator } from "../observability/metricsAggregator.js";
7
- import { SpanStatus, SpanType } from "../observability/types/spanTypes.js";
8
- import { SpanSerializer } from "../observability/utils/spanSerializer.js";
9
6
  import { ATTR, tracers } from "../telemetry/index.js";
10
7
  import { isAbortError } from "../utils/errorHandling.js";
11
8
  import { logger } from "../utils/logger.js";
12
- import { calculateCost } from "../utils/pricing.js";
13
9
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
14
10
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
15
11
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
@@ -85,14 +81,6 @@ export class BaseProvider {
85
81
  */
86
82
  async stream(optionsOrPrompt, analysisSchema) {
87
83
  let options = this.normalizeStreamOptions(optionsOrPrompt);
88
- // Observability: create metrics span for provider.stream
89
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.stream", {
90
- "ai.provider": this.providerName || "unknown",
91
- "ai.model": this.modelName || options.model || "unknown",
92
- "ai.temperature": options.temperature,
93
- "ai.max_tokens": options.maxTokens,
94
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
95
- let metricsSpanRecorded = false;
96
84
  // OTEL span for provider-level stream tracing
97
85
  const otelStreamSpan = tracers.provider.startSpan("neurolink.provider.stream", {
98
86
  kind: SpanKind.CLIENT,
@@ -185,10 +173,6 @@ export class BaseProvider {
185
173
  }
186
174
  }
187
175
  catch (error) {
188
- // Observability: record failed stream span
189
- metricsSpanRecorded = true;
190
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
191
- getMetricsAggregator().recordSpan(endedStreamSpan);
192
176
  otelStreamSpan.setStatus({
193
177
  code: SpanStatusCode.ERROR,
194
178
  message: error instanceof Error ? error.message : String(error),
@@ -197,10 +181,8 @@ export class BaseProvider {
197
181
  throw error;
198
182
  }
199
183
  finally {
200
- // Observability: record successful stream span (only if not already ended via error path)
201
- if (!metricsSpanRecorded) {
202
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.OK);
203
- getMetricsAggregator().recordSpan(endedStreamSpan);
184
+ // End OTEL span on success (only if not already ended via error path)
185
+ if (otelStreamSpan.isRecording()) {
204
186
  otelStreamSpan.setStatus({ code: SpanStatusCode.OK });
205
187
  otelStreamSpan.end();
206
188
  }
@@ -495,13 +477,6 @@ export class BaseProvider {
495
477
  const options = this.normalizeTextOptions(optionsOrPrompt);
496
478
  this.validateOptions(options);
497
479
  const startTime = Date.now();
498
- // Observability: create metrics span for provider.generate
499
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.generate", {
500
- "ai.provider": this.providerName || "unknown",
501
- "ai.model": this.modelName || options.model || "unknown",
502
- "ai.temperature": options.temperature,
503
- "ai.max_tokens": options.maxTokens,
504
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
505
480
  // OTEL span for provider-level generate tracing
506
481
  // Use startActiveSpan pattern via context.with() so child spans become descendants
507
482
  const otelSpan = tracers.provider.startSpan("neurolink.provider.generate", {
@@ -690,33 +665,9 @@ export class BaseProvider {
690
665
  });
691
666
  }
692
667
  }
693
- // Observability: record successful generate span with token/cost data
694
- let enrichedGenerateSpan = { ...metricsSpan };
695
- if (enhancedResult?.usage) {
696
- enrichedGenerateSpan = SpanSerializer.enrichWithTokenUsage(enrichedGenerateSpan, {
697
- promptTokens: enhancedResult.usage.input || 0,
698
- completionTokens: enhancedResult.usage.output || 0,
699
- totalTokens: enhancedResult.usage.total || 0,
700
- });
701
- const cost = calculateCost(this.providerName, this.modelName, {
702
- input: enhancedResult.usage.input || 0,
703
- output: enhancedResult.usage.output || 0,
704
- total: enhancedResult.usage.total || 0,
705
- });
706
- if (cost && cost > 0) {
707
- enrichedGenerateSpan = SpanSerializer.enrichWithCost(enrichedGenerateSpan, {
708
- totalCost: cost,
709
- });
710
- }
711
- }
712
- const endedGenerateSpan = SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
713
- getMetricsAggregator().recordSpan(endedGenerateSpan);
714
668
  return await this.enhanceResult(enhancedResult, options, startTime);
715
669
  }
716
670
  catch (error) {
717
- // Observability: record failed generate span
718
- const endedGenerateSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
719
- getMetricsAggregator().recordSpan(endedGenerateSpan);
720
671
  otelSpan.setStatus({
721
672
  code: SpanStatusCode.ERROR,
722
673
  message: error instanceof Error ? error.message : String(error),
@@ -3,13 +3,9 @@ import { generateText } from "ai";
3
3
  import { directAgentTools } from "../agent/directTools.js";
4
4
  import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
5
5
  import { MiddlewareFactory } from "../middleware/factory.js";
6
- import { getMetricsAggregator } from "../observability/metricsAggregator.js";
7
- import { SpanStatus, SpanType } from "../observability/types/spanTypes.js";
8
- import { SpanSerializer } from "../observability/utils/spanSerializer.js";
9
6
  import { ATTR, tracers } from "../telemetry/index.js";
10
7
  import { isAbortError } from "../utils/errorHandling.js";
11
8
  import { logger } from "../utils/logger.js";
12
- import { calculateCost } from "../utils/pricing.js";
13
9
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
14
10
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
15
11
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
@@ -85,14 +81,6 @@ export class BaseProvider {
85
81
  */
86
82
  async stream(optionsOrPrompt, analysisSchema) {
87
83
  let options = this.normalizeStreamOptions(optionsOrPrompt);
88
- // Observability: create metrics span for provider.stream
89
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.stream", {
90
- "ai.provider": this.providerName || "unknown",
91
- "ai.model": this.modelName || options.model || "unknown",
92
- "ai.temperature": options.temperature,
93
- "ai.max_tokens": options.maxTokens,
94
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
95
- let metricsSpanRecorded = false;
96
84
  // OTEL span for provider-level stream tracing
97
85
  const otelStreamSpan = tracers.provider.startSpan("neurolink.provider.stream", {
98
86
  kind: SpanKind.CLIENT,
@@ -185,10 +173,6 @@ export class BaseProvider {
185
173
  }
186
174
  }
187
175
  catch (error) {
188
- // Observability: record failed stream span
189
- metricsSpanRecorded = true;
190
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
191
- getMetricsAggregator().recordSpan(endedStreamSpan);
192
176
  otelStreamSpan.setStatus({
193
177
  code: SpanStatusCode.ERROR,
194
178
  message: error instanceof Error ? error.message : String(error),
@@ -197,10 +181,8 @@ export class BaseProvider {
197
181
  throw error;
198
182
  }
199
183
  finally {
200
- // Observability: record successful stream span (only if not already ended via error path)
201
- if (!metricsSpanRecorded) {
202
- const endedStreamSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.OK);
203
- getMetricsAggregator().recordSpan(endedStreamSpan);
184
+ // End OTEL span on success (only if not already ended via error path)
185
+ if (otelStreamSpan.isRecording()) {
204
186
  otelStreamSpan.setStatus({ code: SpanStatusCode.OK });
205
187
  otelStreamSpan.end();
206
188
  }
@@ -495,13 +477,6 @@ export class BaseProvider {
495
477
  const options = this.normalizeTextOptions(optionsOrPrompt);
496
478
  this.validateOptions(options);
497
479
  const startTime = Date.now();
498
- // Observability: create metrics span for provider.generate
499
- const metricsSpan = SpanSerializer.createSpan(SpanType.MODEL_GENERATION, "provider.generate", {
500
- "ai.provider": this.providerName || "unknown",
501
- "ai.model": this.modelName || options.model || "unknown",
502
- "ai.temperature": options.temperature,
503
- "ai.max_tokens": options.maxTokens,
504
- }, this._traceContext?.parentSpanId, this._traceContext?.traceId);
505
480
  // OTEL span for provider-level generate tracing
506
481
  // Use startActiveSpan pattern via context.with() so child spans become descendants
507
482
  const otelSpan = tracers.provider.startSpan("neurolink.provider.generate", {
@@ -690,33 +665,9 @@ export class BaseProvider {
690
665
  });
691
666
  }
692
667
  }
693
- // Observability: record successful generate span with token/cost data
694
- let enrichedGenerateSpan = { ...metricsSpan };
695
- if (enhancedResult?.usage) {
696
- enrichedGenerateSpan = SpanSerializer.enrichWithTokenUsage(enrichedGenerateSpan, {
697
- promptTokens: enhancedResult.usage.input || 0,
698
- completionTokens: enhancedResult.usage.output || 0,
699
- totalTokens: enhancedResult.usage.total || 0,
700
- });
701
- const cost = calculateCost(this.providerName, this.modelName, {
702
- input: enhancedResult.usage.input || 0,
703
- output: enhancedResult.usage.output || 0,
704
- total: enhancedResult.usage.total || 0,
705
- });
706
- if (cost && cost > 0) {
707
- enrichedGenerateSpan = SpanSerializer.enrichWithCost(enrichedGenerateSpan, {
708
- totalCost: cost,
709
- });
710
- }
711
- }
712
- const endedGenerateSpan = SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
713
- getMetricsAggregator().recordSpan(endedGenerateSpan);
714
668
  return await this.enhanceResult(enhancedResult, options, startTime);
715
669
  }
716
670
  catch (error) {
717
- // Observability: record failed generate span
718
- const endedGenerateSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
719
- getMetricsAggregator().recordSpan(endedGenerateSpan);
720
671
  otelSpan.setStatus({
721
672
  code: SpanStatusCode.ERROR,
722
673
  message: error instanceof Error ? error.message : String(error),
@@ -1693,7 +1693,11 @@ Current user's request: ${currentInput}`;
1693
1693
  span.spanId = traceCtx.parentSpanId;
1694
1694
  span.parentSpanId = undefined;
1695
1695
  }
1696
- span = SpanSerializer.endSpan(span, SpanStatus.OK);
1696
+ // Mark failed generations with ERROR status so metrics count them correctly
1697
+ const spanStatus = data.success === false || data.error
1698
+ ? SpanStatus.ERROR
1699
+ : SpanStatus.OK;
1700
+ span = SpanSerializer.endSpan(span, spanStatus, data.error ? String(data.error) : undefined);
1697
1701
  span.durationMs = responseTime;
1698
1702
  if (usage) {
1699
1703
  span = SpanSerializer.enrichWithTokenUsage(span, {
@@ -2304,6 +2308,27 @@ Current user's request: ${currentInput}`;
2304
2308
  code: SpanStatusCode.ERROR,
2305
2309
  message: error instanceof Error ? error.message : String(error),
2306
2310
  });
2311
+ // Emit generation:end on error so metrics listeners still record the failure.
2312
+ // Note: variables declared inside try blocks are not accessible in error
2313
+ // handlers, so we extract what we can from the original input.
2314
+ const errProvider = typeof optionsOrPrompt === "object"
2315
+ ? optionsOrPrompt.provider || "unknown"
2316
+ : "unknown";
2317
+ const errModel = typeof optionsOrPrompt === "object"
2318
+ ? optionsOrPrompt.model || "unknown"
2319
+ : "unknown";
2320
+ try {
2321
+ this.emitter.emit("generation:end", {
2322
+ provider: errProvider,
2323
+ model: errModel,
2324
+ responseTime: 0,
2325
+ error: error instanceof Error ? error.message : String(error),
2326
+ success: false,
2327
+ });
2328
+ }
2329
+ catch (emitError) {
2330
+ void emitError; // non-blocking — error event emission is best-effort
2331
+ }
2307
2332
  throw error;
2308
2333
  }
2309
2334
  finally {
@@ -118,7 +118,9 @@ export class LangfuseExporter extends BaseExporter {
118
118
  name: span.name,
119
119
  userId: span.attributes["user.id"],
120
120
  sessionId: span.attributes["session.id"],
121
- metadata: span.attributes,
121
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
122
+ // input, output, error.stack, and other user content to match Braintrust exporter
123
+ metadata: filterSafeMetadata(span.attributes),
122
124
  release: this.release,
123
125
  tags: this.extractTags(span),
124
126
  };
@@ -197,4 +199,6 @@ export class LangfuseExporter extends BaseExporter {
197
199
  return tags;
198
200
  }
199
201
  }
202
+ // Safe metadata filtering imported from shared module to avoid duplication
203
+ import { filterSafeMetadata } from "../utils/safeMetadata.js";
200
204
  //# sourceMappingURL=langfuseExporter.js.map
@@ -35,8 +35,12 @@ export class MetricsAggregator {
35
35
  recordSpan(span) {
36
36
  // Enforce maximum spans limit
37
37
  if (this.spans.length >= this.config.maxSpansRetained) {
38
- this.spans.shift(); // Remove oldest span
39
- // Note: We keep aggregated metrics, only raw spans are trimmed
38
+ const evicted = this.spans.shift(); // Remove oldest span
39
+ // Only trim latencyValues when the evicted span had a duration recorded
40
+ if (evicted?.durationMs !== undefined) {
41
+ this.latencyValues.shift();
42
+ }
43
+ // Note: We keep aggregated metrics, only raw spans and latency values are trimmed
40
44
  }
41
45
  this.spans.push(span);
42
46
  // Update timestamps
@@ -66,6 +66,8 @@ export class RedactionProcessor {
66
66
  "credentials",
67
67
  "private_key",
68
68
  "privateKey",
69
+ "stack",
70
+ "error.stack",
69
71
  ]);
70
72
  this.redactedValue = config?.redactedValue ?? "[REDACTED]";
71
73
  }
@@ -228,11 +230,25 @@ export class BatchProcessor {
228
230
  this.flush();
229
231
  }, this.flushIntervalMs);
230
232
  }
233
+ // Note: flush() is intentionally synchronous. The onBatchReady callback is
234
+ // typed as `(spans: SpanData[]) => void` — callers must not pass async
235
+ // exporters. If async export is needed, the callback should handle its own
236
+ // error reporting (e.g. fire-and-forget with promise error handlers).
231
237
  flush() {
232
238
  if (this.batch.length > 0 && this.onBatchReady) {
233
239
  const spans = [...this.batch];
234
- this.batch = [];
235
- this.onBatchReady(spans);
240
+ try {
241
+ this.onBatchReady(spans);
242
+ this.batch = [];
243
+ }
244
+ catch (flushError) {
245
+ // Keep spans for next flush attempt, but cap backlog growth
246
+ void flushError; // acknowledged — error is expected during exporter outages
247
+ const maxBacklog = this.batchSize * 20;
248
+ if (this.batch.length > maxBacklog) {
249
+ this.batch = this.batch.slice(this.batch.length - maxBacklog);
250
+ }
251
+ }
236
252
  }
237
253
  }
238
254
  async shutdown() {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ import type { SpanAttributes } from "../types/spanTypes.js";
9
+ export declare const SAFE_METADATA_KEYS: Set<string>;
10
+ export declare function filterSafeMetadata(attributes: SpanAttributes): Record<string, unknown>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ // Only ai.* keys are forwarded as metadata. Stream metrics (chunk_count,
9
+ // content_length) should be accessed via span attributes directly, not via
10
+ // metadata sent to third-party backends.
11
+ export const SAFE_METADATA_KEYS = new Set([
12
+ "ai.provider",
13
+ "ai.model",
14
+ "ai.temperature",
15
+ "ai.max_tokens",
16
+ ]);
17
+ export function filterSafeMetadata(attributes) {
18
+ const filtered = {};
19
+ for (const key of SAFE_METADATA_KEYS) {
20
+ if (attributes[key] !== undefined) {
21
+ filtered[key] = attributes[key];
22
+ }
23
+ }
24
+ return filtered;
25
+ }
26
+ //# sourceMappingURL=safeMetadata.js.map
@@ -104,7 +104,9 @@ export class SpanSerializer {
104
104
  name: span.name,
105
105
  startTime: span.startTime,
106
106
  endTime: span.endTime,
107
- metadata: { ...span.attributes },
107
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
108
+ // input, output, error.stack, and other user content for PII safety
109
+ metadata: filterSafeMetadata(span.attributes),
108
110
  level: span.status === SpanStatus.ERROR ? "ERROR" : "DEFAULT",
109
111
  statusMessage: span.statusMessage,
110
112
  input: span.attributes["input"],
@@ -284,4 +286,6 @@ export class SpanSerializer {
284
286
  });
285
287
  }
286
288
  }
289
+ // Safe metadata filtering imported from shared module to avoid duplication
290
+ import { filterSafeMetadata } from "./safeMetadata.js";
287
291
  //# sourceMappingURL=spanSerializer.js.map
@@ -1259,9 +1259,7 @@ export class GoogleVertexProvider extends BaseProvider {
1259
1259
  },
1260
1260
  {
1261
1261
  role: "model",
1262
- parts: [
1263
- { text: "Understood. I will follow these instructions." },
1264
- ],
1262
+ parts: [{ text: "OK" }],
1265
1263
  },
1266
1264
  ...currentContents,
1267
1265
  ];
@@ -1435,9 +1433,7 @@ export class GoogleVertexProvider extends BaseProvider {
1435
1433
  },
1436
1434
  {
1437
1435
  role: "model",
1438
- parts: [
1439
- { text: "Understood. I will follow these instructions." },
1440
- ],
1436
+ parts: [{ text: "OK" }],
1441
1437
  },
1442
1438
  ...currentContents,
1443
1439
  ];
package/dist/neurolink.js CHANGED
@@ -1693,7 +1693,11 @@ Current user's request: ${currentInput}`;
1693
1693
  span.spanId = traceCtx.parentSpanId;
1694
1694
  span.parentSpanId = undefined;
1695
1695
  }
1696
- span = SpanSerializer.endSpan(span, SpanStatus.OK);
1696
+ // Mark failed generations with ERROR status so metrics count them correctly
1697
+ const spanStatus = data.success === false || data.error
1698
+ ? SpanStatus.ERROR
1699
+ : SpanStatus.OK;
1700
+ span = SpanSerializer.endSpan(span, spanStatus, data.error ? String(data.error) : undefined);
1697
1701
  span.durationMs = responseTime;
1698
1702
  if (usage) {
1699
1703
  span = SpanSerializer.enrichWithTokenUsage(span, {
@@ -2304,6 +2308,27 @@ Current user's request: ${currentInput}`;
2304
2308
  code: SpanStatusCode.ERROR,
2305
2309
  message: error instanceof Error ? error.message : String(error),
2306
2310
  });
2311
+ // Emit generation:end on error so metrics listeners still record the failure.
2312
+ // Note: variables declared inside try blocks are not accessible in error
2313
+ // handlers, so we extract what we can from the original input.
2314
+ const errProvider = typeof optionsOrPrompt === "object"
2315
+ ? optionsOrPrompt.provider || "unknown"
2316
+ : "unknown";
2317
+ const errModel = typeof optionsOrPrompt === "object"
2318
+ ? optionsOrPrompt.model || "unknown"
2319
+ : "unknown";
2320
+ try {
2321
+ this.emitter.emit("generation:end", {
2322
+ provider: errProvider,
2323
+ model: errModel,
2324
+ responseTime: 0,
2325
+ error: error instanceof Error ? error.message : String(error),
2326
+ success: false,
2327
+ });
2328
+ }
2329
+ catch (emitError) {
2330
+ void emitError; // non-blocking — error event emission is best-effort
2331
+ }
2307
2332
  throw error;
2308
2333
  }
2309
2334
  finally {
@@ -118,7 +118,9 @@ export class LangfuseExporter extends BaseExporter {
118
118
  name: span.name,
119
119
  userId: span.attributes["user.id"],
120
120
  sessionId: span.attributes["session.id"],
121
- metadata: span.attributes,
121
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
122
+ // input, output, error.stack, and other user content to match Braintrust exporter
123
+ metadata: filterSafeMetadata(span.attributes),
122
124
  release: this.release,
123
125
  tags: this.extractTags(span),
124
126
  };
@@ -197,3 +199,5 @@ export class LangfuseExporter extends BaseExporter {
197
199
  return tags;
198
200
  }
199
201
  }
202
+ // Safe metadata filtering imported from shared module to avoid duplication
203
+ import { filterSafeMetadata } from "../utils/safeMetadata.js";
@@ -35,8 +35,12 @@ export class MetricsAggregator {
35
35
  recordSpan(span) {
36
36
  // Enforce maximum spans limit
37
37
  if (this.spans.length >= this.config.maxSpansRetained) {
38
- this.spans.shift(); // Remove oldest span
39
- // Note: We keep aggregated metrics, only raw spans are trimmed
38
+ const evicted = this.spans.shift(); // Remove oldest span
39
+ // Only trim latencyValues when the evicted span had a duration recorded
40
+ if (evicted?.durationMs !== undefined) {
41
+ this.latencyValues.shift();
42
+ }
43
+ // Note: We keep aggregated metrics, only raw spans and latency values are trimmed
40
44
  }
41
45
  this.spans.push(span);
42
46
  // Update timestamps
@@ -66,6 +66,8 @@ export class RedactionProcessor {
66
66
  "credentials",
67
67
  "private_key",
68
68
  "privateKey",
69
+ "stack",
70
+ "error.stack",
69
71
  ]);
70
72
  this.redactedValue = config?.redactedValue ?? "[REDACTED]";
71
73
  }
@@ -228,11 +230,25 @@ export class BatchProcessor {
228
230
  this.flush();
229
231
  }, this.flushIntervalMs);
230
232
  }
233
+ // Note: flush() is intentionally synchronous. The onBatchReady callback is
234
+ // typed as `(spans: SpanData[]) => void` — callers must not pass async
235
+ // exporters. If async export is needed, the callback should handle its own
236
+ // error reporting (e.g. fire-and-forget with promise error handlers).
231
237
  flush() {
232
238
  if (this.batch.length > 0 && this.onBatchReady) {
233
239
  const spans = [...this.batch];
234
- this.batch = [];
235
- this.onBatchReady(spans);
240
+ try {
241
+ this.onBatchReady(spans);
242
+ this.batch = [];
243
+ }
244
+ catch (flushError) {
245
+ // Keep spans for next flush attempt, but cap backlog growth
246
+ void flushError; // acknowledged — error is expected during exporter outages
247
+ const maxBacklog = this.batchSize * 20;
248
+ if (this.batch.length > maxBacklog) {
249
+ this.batch = this.batch.slice(this.batch.length - maxBacklog);
250
+ }
251
+ }
236
252
  }
237
253
  }
238
254
  async shutdown() {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ import type { SpanAttributes } from "../types/spanTypes.js";
9
+ export declare const SAFE_METADATA_KEYS: Set<string>;
10
+ export declare function filterSafeMetadata(attributes: SpanAttributes): Record<string, unknown>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Safe metadata filtering for observability exporters.
3
+ *
4
+ * Only these attribute keys are forwarded to third-party backends as trace
5
+ * metadata. User prompts (input), LLM responses (output), error stacks, and
6
+ * any other potentially sensitive data are excluded to prevent PII leaks.
7
+ */
8
+ // Only ai.* keys are forwarded as metadata. Stream metrics (chunk_count,
9
+ // content_length) should be accessed via span attributes directly, not via
10
+ // metadata sent to third-party backends.
11
+ export const SAFE_METADATA_KEYS = new Set([
12
+ "ai.provider",
13
+ "ai.model",
14
+ "ai.temperature",
15
+ "ai.max_tokens",
16
+ ]);
17
+ export function filterSafeMetadata(attributes) {
18
+ const filtered = {};
19
+ for (const key of SAFE_METADATA_KEYS) {
20
+ if (attributes[key] !== undefined) {
21
+ filtered[key] = attributes[key];
22
+ }
23
+ }
24
+ return filtered;
25
+ }
@@ -104,7 +104,9 @@ export class SpanSerializer {
104
104
  name: span.name,
105
105
  startTime: span.startTime,
106
106
  endTime: span.endTime,
107
- metadata: { ...span.attributes },
107
+ // Only pick safe, non-PII attributes for metadata — intentionally excludes
108
+ // input, output, error.stack, and other user content for PII safety
109
+ metadata: filterSafeMetadata(span.attributes),
108
110
  level: span.status === SpanStatus.ERROR ? "ERROR" : "DEFAULT",
109
111
  statusMessage: span.statusMessage,
110
112
  input: span.attributes["input"],
@@ -284,3 +286,5 @@ export class SpanSerializer {
284
286
  });
285
287
  }
286
288
  }
289
+ // Safe metadata filtering imported from shared module to avoid duplication
290
+ import { filterSafeMetadata } from "./safeMetadata.js";
@@ -1259,9 +1259,7 @@ export class GoogleVertexProvider extends BaseProvider {
1259
1259
  },
1260
1260
  {
1261
1261
  role: "model",
1262
- parts: [
1263
- { text: "Understood. I will follow these instructions." },
1264
- ],
1262
+ parts: [{ text: "OK" }],
1265
1263
  },
1266
1264
  ...currentContents,
1267
1265
  ];
@@ -1435,9 +1433,7 @@ export class GoogleVertexProvider extends BaseProvider {
1435
1433
  },
1436
1434
  {
1437
1435
  role: "model",
1438
- parts: [
1439
- { text: "Understood. I will follow these instructions." },
1440
- ],
1436
+ parts: [{ text: "OK" }],
1441
1437
  },
1442
1438
  ...currentContents,
1443
1439
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.25.0",
3
+ "version": "9.25.1",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",