@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 +6 -0
- package/dist/core/baseProvider.js +2 -51
- package/dist/lib/core/baseProvider.js +2 -51
- package/dist/lib/neurolink.js +26 -1
- package/dist/lib/observability/exporters/langfuseExporter.js +5 -1
- package/dist/lib/observability/metricsAggregator.js +6 -2
- package/dist/lib/observability/spanProcessor.js +18 -2
- package/dist/lib/observability/utils/safeMetadata.d.ts +10 -0
- package/dist/lib/observability/utils/safeMetadata.js +26 -0
- package/dist/lib/observability/utils/spanSerializer.js +5 -1
- package/dist/lib/providers/googleVertex.js +2 -6
- package/dist/neurolink.js +26 -1
- package/dist/observability/exporters/langfuseExporter.js +5 -1
- package/dist/observability/metricsAggregator.js +6 -2
- package/dist/observability/spanProcessor.js +18 -2
- package/dist/observability/utils/safeMetadata.d.ts +10 -0
- package/dist/observability/utils/safeMetadata.js +25 -0
- package/dist/observability/utils/spanSerializer.js +5 -1
- package/dist/providers/googleVertex.js +2 -6
- package/package.json +1 -1
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
|
-
//
|
|
201
|
-
if (
|
|
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
|
-
//
|
|
201
|
-
if (
|
|
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),
|
package/dist/lib/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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|