@juspay/neurolink 12.58.1 → 13.0.0

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +14 -3
  2. package/dist/auth/anthropicOAuth.js +3 -3
  3. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  4. package/dist/auth/sessionManager.js +4 -4
  5. package/dist/auth/tokenStore.js +4 -4
  6. package/dist/browser/neurolink.min.js +474 -473
  7. package/dist/cli/commands/observability.d.ts +1 -6
  8. package/dist/cli/commands/observability.js +0 -106
  9. package/dist/cli/commands/proxy.js +9 -18
  10. package/dist/cli/commands/telemetry.d.ts +3 -13
  11. package/dist/cli/commands/telemetry.js +7 -354
  12. package/dist/cli/loop/session.js +1 -1
  13. package/dist/client/httpClient.js +1 -1
  14. package/dist/client/sseClient.js +1 -1
  15. package/dist/client/streamingClient.js +2 -2
  16. package/dist/client/wsClient.js +1 -1
  17. package/dist/constants/langfuse.d.ts +25 -0
  18. package/dist/constants/langfuse.js +38 -0
  19. package/dist/constants/telemetry.d.ts +20 -0
  20. package/dist/constants/telemetry.js +34 -0
  21. package/dist/core/baseProvider.d.ts +2 -1
  22. package/dist/core/baseProvider.js +4 -11
  23. package/dist/core/conversationMemoryManager.js +5 -8
  24. package/dist/core/factory.js +1 -1
  25. package/dist/core/geminiLoopAdapter.d.ts +8 -1
  26. package/dist/core/geminiLoopAdapter.js +25 -1
  27. package/dist/core/loopEngine.js +69 -40
  28. package/dist/core/modules/MessageBuilder.js +2 -2
  29. package/dist/core/modules/StreamHandler.js +2 -2
  30. package/dist/core/modules/TelemetryHandler.d.ts +2 -18
  31. package/dist/core/modules/TelemetryHandler.js +1 -66
  32. package/dist/core/modules/ToolsManager.js +40 -31
  33. package/dist/core/nativeGenerateLoop.js +78 -8
  34. package/dist/core/redisConversationMemoryManager.js +8 -15
  35. package/dist/core/toolExecutionGuards.js +1 -13
  36. package/dist/index.d.ts +5 -12
  37. package/dist/index.js +5 -37
  38. package/dist/mcp/batching/requestBatcher.js +1 -1
  39. package/dist/mcp/externalServerManager.js +3 -21
  40. package/dist/mcp/toolDiscoveryService.js +4 -16
  41. package/dist/mcp/toolRegistry.js +2 -5
  42. package/dist/memory/memoryRetrievalTools.js +1 -1
  43. package/dist/neurolink.d.ts +16 -27
  44. package/dist/neurolink.js +788 -792
  45. package/dist/observability/exporterSpanProcessor.d.ts +50 -0
  46. package/dist/observability/exporterSpanProcessor.js +181 -0
  47. package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
  48. package/dist/observability/exporters/langfuseExporter.js +101 -154
  49. package/dist/observability/index.d.ts +1 -0
  50. package/dist/observability/index.js +1 -0
  51. package/dist/observability/langfuseMasking.d.ts +8 -0
  52. package/dist/observability/langfuseMasking.js +47 -0
  53. package/dist/observability/otlpEncoding.d.ts +20 -0
  54. package/dist/observability/otlpEncoding.js +85 -0
  55. package/dist/providers/amazonBedrock/client.js +142 -176
  56. package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
  57. package/dist/providers/amazonSagemaker.js +118 -80
  58. package/dist/providers/anthropic/client.js +3 -52
  59. package/dist/providers/anthropic/loopAdapter.js +24 -1
  60. package/dist/providers/anthropic/rateLimitCapture.js +9 -12
  61. package/dist/providers/googleAiStudio/client.d.ts +4 -5
  62. package/dist/providers/googleAiStudio/client.js +38 -40
  63. package/dist/providers/googleVertex/client.d.ts +2 -7
  64. package/dist/providers/googleVertex/client.js +282 -378
  65. package/dist/providers/googleVertex/constants.js +2 -2
  66. package/dist/providers/litellm/client.d.ts +2 -9
  67. package/dist/providers/litellm/client.js +1 -63
  68. package/dist/providers/openAI/client.d.ts +1 -8
  69. package/dist/providers/openAI/client.js +0 -65
  70. package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
  71. package/dist/providers/openaiChatCompletionsBase.js +61 -4
  72. package/dist/providers/replicate.js +22 -4
  73. package/dist/proxy/codexUsage.js +7 -0
  74. package/dist/proxy/proxyFetch.js +19 -25
  75. package/dist/proxy/proxyTracer.d.ts +14 -7
  76. package/dist/proxy/proxyTracer.js +129 -83
  77. package/dist/proxy/proxyTranslationEngine.js +0 -1
  78. package/dist/proxy/requestLogger.d.ts +1 -1
  79. package/dist/proxy/requestLogger.js +39 -45
  80. package/dist/server/middleware/common.js +1 -1
  81. package/dist/server/routes/agentRoutes.js +4 -4
  82. package/dist/server/routes/claudeProxyRoutes.js +99 -196
  83. package/dist/server/routes/codexProxyRoutes.js +621 -591
  84. package/dist/server/routes/healthRoutes.js +6 -6
  85. package/dist/server/routes/mcpRoutes.js +7 -7
  86. package/dist/server/routes/memoryRoutes.js +7 -7
  87. package/dist/server/routes/openApiRoutes.js +3 -3
  88. package/dist/server/routes/toolRoutes.js +5 -5
  89. package/dist/server/routes/transcribeRoutes.js +1 -1
  90. package/dist/server/utils/validation.d.ts +3 -3
  91. package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
  92. package/dist/services/server/ai/observability/instrumentation.js +220 -1267
  93. package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
  94. package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
  95. package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
  96. package/dist/services/server/ai/observability/otlpSetup.js +58 -0
  97. package/dist/session/globalSessionState.js +0 -11
  98. package/dist/telemetry/attributes.d.ts +23 -46
  99. package/dist/telemetry/attributes.js +76 -162
  100. package/dist/telemetry/chatMessages.d.ts +13 -0
  101. package/dist/telemetry/chatMessages.js +59 -0
  102. package/dist/telemetry/index.d.ts +5 -19
  103. package/dist/telemetry/index.js +5 -26
  104. package/dist/telemetry/observation.d.ts +37 -0
  105. package/dist/telemetry/observation.js +112 -0
  106. package/dist/telemetry/observationNames.d.ts +18 -0
  107. package/dist/telemetry/observationNames.js +24 -0
  108. package/dist/telemetry/spanSanitizer.d.ts +39 -0
  109. package/dist/telemetry/spanSanitizer.js +92 -0
  110. package/dist/telemetry/spanSerialization.d.ts +13 -0
  111. package/dist/telemetry/spanSerialization.js +46 -0
  112. package/dist/telemetry/tracers.d.ts +37 -18
  113. package/dist/telemetry/tracers.js +97 -19
  114. package/dist/telemetry/withSpan.d.ts +47 -25
  115. package/dist/telemetry/withSpan.js +158 -71
  116. package/dist/types/cli.d.ts +0 -11
  117. package/dist/types/decision.d.ts +3 -0
  118. package/dist/types/exporter.d.ts +54 -0
  119. package/dist/types/exporter.js +0 -4
  120. package/dist/types/generate.d.ts +4 -1
  121. package/dist/types/isolatedAgent.d.ts +5 -4
  122. package/dist/types/loopEngine.d.ts +18 -28
  123. package/dist/types/observability.d.ts +127 -160
  124. package/dist/types/providers.d.ts +15 -0
  125. package/dist/types/proxy.d.ts +41 -2
  126. package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
  127. package/dist/utils/anthropicTraceSanitizer.js +3 -1
  128. package/dist/utils/conversationMemory.js +4 -4
  129. package/dist/utils/fileDetector.js +1 -1
  130. package/dist/utils/messageBuilder.js +1 -1
  131. package/dist/utils/noOutputSentinel.d.ts +4 -16
  132. package/dist/utils/noOutputSentinel.js +12 -28
  133. package/dist/utils/providerRetry.js +4 -4
  134. package/dist/voice/livekit/brain.js +1 -1
  135. package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
  136. package/dist/voice/livekit/realtimeMemory.js +66 -1
  137. package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
  138. package/docs-site/static/search-index.json +42 -63
  139. package/package.json +10 -4
  140. package/dist/telemetry/telemetryService.d.ts +0 -79
  141. package/dist/telemetry/telemetryService.js +0 -572
  142. package/dist/utils/observabilityHelpers.d.ts +0 -27
  143. package/dist/utils/observabilityHelpers.js +0 -47
@@ -0,0 +1,50 @@
1
+ import { type Context } from "@opentelemetry/api";
2
+ import type { ReadableSpan, Span, SpanProcessor as OtelSpanProcessor } from "@opentelemetry/sdk-trace-base";
3
+ import type { ExporterRegistry } from "./exporterRegistry.js";
4
+ /**
5
+ * @description OpenTelemetry span processor that turns every observation span into `SpanData` and hands it to an `ExporterRegistry`, so one instrumentation feeds Langfuse, Datadog, Sentry and every other registered exporter. Spans that are not observations are skipped and their children are re-parented onto the nearest exported ancestor. Delivery never blocks the span's thread.
6
+ */
7
+ export declare class ExporterSpanProcessor implements OtelSpanProcessor {
8
+ private readonly registry;
9
+ private readonly open;
10
+ private readonly ended;
11
+ /**
12
+ * @description Creates a processor bound to a registry.
13
+ * @param registry {ExporterRegistry} - Registry that receives every exported span.
14
+ */
15
+ constructor(registry: ExporterRegistry);
16
+ /**
17
+ * @description Copies the propagated Langfuse trace attributes (user, session, trace name, tags, metadata) from the parent context onto the new span and remembers it until it ends.
18
+ * @param span {Span} - Span that just started.
19
+ * @param parentContext {Context} - Context the span was started in.
20
+ * @returns {void} Nothing.
21
+ */
22
+ onStart(span: Span, parentContext: Context): void;
23
+ /**
24
+ * @description Exports an ended observation span without waiting for the exporters.
25
+ * @param span {ReadableSpan} - Span that just ended.
26
+ * @returns {void} Nothing.
27
+ */
28
+ onEnd(span: ReadableSpan): void;
29
+ /**
30
+ * @description Flushes every exporter in the registry.
31
+ * @returns {Promise<void>} Resolves when all exporters have flushed.
32
+ */
33
+ forceFlush(): Promise<void>;
34
+ /**
35
+ * @description Shuts down every exporter in the registry.
36
+ * @returns {Promise<void>} Resolves when all exporters have shut down.
37
+ */
38
+ shutdown(): Promise<void>;
39
+ private exportQuietly;
40
+ private remember;
41
+ private nearestExportedAncestor;
42
+ }
43
+ /**
44
+ * @description Creates a span processor that feeds NeuroLink's observation spans to an exporter registry. Add it to a host `TracerProvider` to deliver spans through the registered exporters (such as `LangfuseExporter`) instead of `LangfuseSpanProcessor`.
45
+ * @param registry {ExporterRegistry} - Registry to export to; defaults to the process-wide registry.
46
+ * @returns {ExporterSpanProcessor} A processor ready to be added to a TracerProvider.
47
+ * @example
48
+ * const provider = new NodeTracerProvider({ spanProcessors: [createExporterSpanProcessor()] });
49
+ */
50
+ export declare function createExporterSpanProcessor(registry?: ExporterRegistry): ExporterSpanProcessor;
@@ -0,0 +1,181 @@
1
+ import { getPropagatedAttributesFromContext } from "@langfuse/core";
2
+ import { SpanStatusCode } from "@opentelemetry/api";
3
+ import { MAX_REMEMBERED_SPANS } from "../constants/telemetry.js";
4
+ import { SpanStatus, SpanType } from "../types/index.js";
5
+ import { logger } from "../utils/logger.js";
6
+ import { getExporterRegistry } from "./exporterRegistry.js";
7
+ const LANGFUSE_OBSERVATION_TYPE = "langfuse.observation.type";
8
+ const GEN_AI_OBSERVATION_TYPE = "gen_ai.observation.type";
9
+ const GEN_AI_OPERATION = "gen_ai.operation.name";
10
+ const OBSERVATION_SPAN_TYPES = {
11
+ agent: SpanType.AGENT_RUN,
12
+ tool: SpanType.TOOL_CALL,
13
+ chain: SpanType.WORKFLOW_STEP,
14
+ retriever: SpanType.RETRIEVAL,
15
+ evaluator: SpanType.EVALUATION,
16
+ embedding: SpanType.EMBEDDING,
17
+ };
18
+ function isObservation(span) {
19
+ return (span.attributes[LANGFUSE_OBSERVATION_TYPE] !== undefined ||
20
+ span.attributes[GEN_AI_OBSERVATION_TYPE] !== undefined ||
21
+ Object.keys(span.attributes).some((key) => key.startsWith("gen_ai.")));
22
+ }
23
+ function spanTypeOf(span) {
24
+ const observationType = String(span.attributes[LANGFUSE_OBSERVATION_TYPE] ?? "").toLowerCase();
25
+ if (observationType === "generation") {
26
+ return span.attributes[GEN_AI_OPERATION] === "decide"
27
+ ? SpanType.MODEL_DECISION
28
+ : SpanType.MODEL_GENERATION;
29
+ }
30
+ return OBSERVATION_SPAN_TYPES[observationType] ?? SpanType.CUSTOM;
31
+ }
32
+ const STATUS_BY_CODE = {
33
+ [SpanStatusCode.UNSET]: SpanStatus.UNSET,
34
+ [SpanStatusCode.OK]: SpanStatus.OK,
35
+ [SpanStatusCode.ERROR]: SpanStatus.ERROR,
36
+ };
37
+ function toMillis(time) {
38
+ return time[0] * 1_000 + time[1] / 1_000_000;
39
+ }
40
+ function toIso(time) {
41
+ return new Date(toMillis(time)).toISOString();
42
+ }
43
+ function toSpanData(span, parentSpanId) {
44
+ const { traceId, spanId } = span.spanContext();
45
+ return {
46
+ spanId,
47
+ traceId,
48
+ ...(parentSpanId && { parentSpanId }),
49
+ type: spanTypeOf(span),
50
+ name: span.name,
51
+ startTime: toIso(span.startTime),
52
+ endTime: toIso(span.endTime),
53
+ durationMs: toMillis(span.duration),
54
+ status: STATUS_BY_CODE[span.status.code],
55
+ ...(span.status.message && { statusMessage: span.status.message }),
56
+ attributes: { ...span.attributes },
57
+ events: span.events.map((event) => ({
58
+ name: event.name,
59
+ timestamp: toIso(event.time),
60
+ ...(event.attributes && { attributes: { ...event.attributes } }),
61
+ })),
62
+ links: span.links.map((link) => ({
63
+ traceId: link.context.traceId,
64
+ spanId: link.context.spanId,
65
+ ...(link.attributes && { attributes: { ...link.attributes } }),
66
+ })),
67
+ };
68
+ }
69
+ /**
70
+ * @description OpenTelemetry span processor that turns every observation span into `SpanData` and hands it to an `ExporterRegistry`, so one instrumentation feeds Langfuse, Datadog, Sentry and every other registered exporter. Spans that are not observations are skipped and their children are re-parented onto the nearest exported ancestor. Delivery never blocks the span's thread.
71
+ */
72
+ export class ExporterSpanProcessor {
73
+ registry;
74
+ open = new Map();
75
+ ended = new Map();
76
+ /**
77
+ * @description Creates a processor bound to a registry.
78
+ * @param registry {ExporterRegistry} - Registry that receives every exported span.
79
+ */
80
+ constructor(registry) {
81
+ this.registry = registry;
82
+ }
83
+ /**
84
+ * @description Copies the propagated Langfuse trace attributes (user, session, trace name, tags, metadata) from the parent context onto the new span and remembers it until it ends.
85
+ * @param span {Span} - Span that just started.
86
+ * @param parentContext {Context} - Context the span was started in.
87
+ * @returns {void} Nothing.
88
+ */
89
+ onStart(span, parentContext) {
90
+ span.setAttributes(getPropagatedAttributesFromContext(parentContext));
91
+ this.open.set(span.spanContext().spanId, span);
92
+ if (this.open.size > MAX_REMEMBERED_SPANS) {
93
+ const oldest = this.open.keys().next().value;
94
+ if (oldest !== undefined) {
95
+ this.open.delete(oldest);
96
+ }
97
+ }
98
+ }
99
+ /**
100
+ * @description Exports an ended observation span without waiting for the exporters.
101
+ * @param span {ReadableSpan} - Span that just ended.
102
+ * @returns {void} Nothing.
103
+ */
104
+ onEnd(span) {
105
+ const { spanId } = span.spanContext();
106
+ this.open.delete(spanId);
107
+ const observation = isObservation(span);
108
+ this.remember(spanId, span.parentSpanContext?.spanId, observation);
109
+ if (!observation) {
110
+ return;
111
+ }
112
+ const spanData = toSpanData(span, this.nearestExportedAncestor(span));
113
+ void this.exportQuietly(spanData);
114
+ }
115
+ /**
116
+ * @description Flushes every exporter in the registry.
117
+ * @returns {Promise<void>} Resolves when all exporters have flushed.
118
+ */
119
+ forceFlush() {
120
+ return this.registry.flushAll();
121
+ }
122
+ /**
123
+ * @description Shuts down every exporter in the registry.
124
+ * @returns {Promise<void>} Resolves when all exporters have shut down.
125
+ */
126
+ shutdown() {
127
+ return this.registry.shutdownAll();
128
+ }
129
+ async exportQuietly(spanData) {
130
+ try {
131
+ await this.registry.exportToAll(spanData);
132
+ }
133
+ catch (error) {
134
+ logger.debug("[ExporterSpanProcessor] Export failed", {
135
+ spanId: spanData.spanId,
136
+ error: error instanceof Error ? error.message : String(error),
137
+ });
138
+ }
139
+ }
140
+ remember(spanId, parentSpanId, observation) {
141
+ this.ended.set(spanId, { parentSpanId, observation });
142
+ if (this.ended.size > MAX_REMEMBERED_SPANS) {
143
+ const oldest = this.ended.keys().next().value;
144
+ if (oldest !== undefined) {
145
+ this.ended.delete(oldest);
146
+ }
147
+ }
148
+ }
149
+ nearestExportedAncestor(span) {
150
+ let candidate = span.parentSpanContext?.spanId;
151
+ while (candidate) {
152
+ const live = this.open.get(candidate);
153
+ if (live) {
154
+ if (isObservation(live)) {
155
+ return candidate;
156
+ }
157
+ candidate = live.parentSpanContext?.spanId;
158
+ continue;
159
+ }
160
+ const finished = this.ended.get(candidate);
161
+ if (!finished) {
162
+ return undefined;
163
+ }
164
+ if (finished.observation) {
165
+ return candidate;
166
+ }
167
+ candidate = finished.parentSpanId;
168
+ }
169
+ return undefined;
170
+ }
171
+ }
172
+ /**
173
+ * @description Creates a span processor that feeds NeuroLink's observation spans to an exporter registry. Add it to a host `TracerProvider` to deliver spans through the registered exporters (such as `LangfuseExporter`) instead of `LangfuseSpanProcessor`.
174
+ * @param registry {ExporterRegistry} - Registry to export to; defaults to the process-wide registry.
175
+ * @returns {ExporterSpanProcessor} A processor ready to be added to a TracerProvider.
176
+ * @example
177
+ * const provider = new NodeTracerProvider({ spanProcessors: [createExporterSpanProcessor()] });
178
+ */
179
+ export function createExporterSpanProcessor(registry = getExporterRegistry()) {
180
+ return new ExporterSpanProcessor(registry);
181
+ }
@@ -1,48 +1,27 @@
1
- /**
2
- * Langfuse Exporter
3
- * Exports spans to Langfuse observability platform
4
- */
5
1
  import type { ExporterHealthStatus, ExportResult, LangfuseExporterConfig, SpanData } from "../../types/index.js";
6
2
  import { BaseExporter } from "./baseExporter.js";
7
3
  /**
8
- * Langfuse exporter for LLM observability
9
- * Supports traces, generations, spans, and scores
4
+ * @description Exporter that delivers observation spans to Langfuse's OTLP/HTTP JSON endpoint (`POST /api/public/otel/v1/traces`), which keeps native observation types and identity on every observation. Spans are queued without blocking and sent in batches; failed requests are retried with backoff when the failure is transient (429, 5xx, network).
10
5
  */
11
6
  export declare class LangfuseExporter extends BaseExporter {
12
- private readonly publicKey;
13
- private readonly secretKey;
7
+ private readonly langfuse;
8
+ private readonly authorization;
14
9
  private readonly baseUrl;
15
- private readonly release?;
16
- private readonly redactIO;
17
- constructor(config: LangfuseExporterConfig);
10
+ private readonly policy;
11
+ private readonly inflight;
12
+ /**
13
+ * @description Creates an exporter for one Langfuse project.
14
+ * @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking and batching options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
15
+ */
16
+ constructor(langfuse: LangfuseExporterConfig);
18
17
  initialize(): Promise<void>;
19
18
  exportSpan(span: SpanData): Promise<ExportResult>;
20
19
  exportBatch(spans: SpanData[]): Promise<ExportResult>;
21
20
  flush(): Promise<void>;
21
+ private exportQuietly;
22
22
  shutdown(): Promise<void>;
23
23
  healthCheck(): Promise<ExporterHealthStatus>;
24
- /**
25
- * Verify connectivity to Langfuse API
26
- */
27
24
  protected ping(): Promise<void>;
28
- /**
29
- * Create a Langfuse trace
30
- */
31
- private createTrace;
32
- /**
33
- * Create a Langfuse span
34
- */
35
- private createSpan;
36
- /**
37
- * Create a Langfuse generation (for LLM calls)
38
- */
39
- private createGeneration;
40
- /**
41
- * Make API call to Langfuse
42
- */
43
- private apiCall;
44
- /**
45
- * Extract tags from span attributes
46
- */
47
- private extractTags;
25
+ private deliver;
26
+ private post;
48
27
  }
@@ -1,85 +1,78 @@
1
- /**
2
- * Langfuse Exporter
3
- * Exports spans to Langfuse observability platform
4
- */
5
- import { SpanType } from "../../types/index.js";
6
- import { SpanSerializer } from "../utils/spanSerializer.js";
1
+ import { DEFAULT_BASE_URL, DEFAULT_FLUSH_AT, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_TIMEOUT_MS, HEALTH_PATH, TRACES_PATH, } from "../../constants/langfuse.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { chunkOtlpSpans, toOtlpAttributes, toOtlpSpan, } from "../otlpEncoding.js";
7
4
  import { BaseExporter } from "./baseExporter.js";
5
+ let credentialsWarned = false;
6
+ function isRetryable(status) {
7
+ return status === 429 || status >= 500;
8
+ }
8
9
  /**
9
- * Langfuse exporter for LLM observability
10
- * Supports traces, generations, spans, and scores
10
+ * @description Exporter that delivers observation spans to Langfuse's OTLP/HTTP JSON endpoint (`POST /api/public/otel/v1/traces`), which keeps native observation types and identity on every observation. Spans are queued without blocking and sent in batches; failed requests are retried with backoff when the failure is transient (429, 5xx, network).
11
11
  */
12
12
  export class LangfuseExporter extends BaseExporter {
13
- publicKey;
14
- secretKey;
13
+ langfuse;
14
+ authorization;
15
15
  baseUrl;
16
- release;
17
- redactIO;
18
- constructor(config) {
19
- super("langfuse", config);
20
- this.publicKey = config.publicKey;
21
- this.secretKey = config.secretKey;
22
- this.baseUrl = config.baseUrl ?? "https://cloud.langfuse.com";
23
- this.release = config.release;
24
- this.redactIO = config.redactIO ?? false;
16
+ policy;
17
+ inflight = new Set();
18
+ /**
19
+ * @description Creates an exporter for one Langfuse project.
20
+ * @param config {LangfuseExporterConfig} - Credentials, endpoint, environment, release, masking and batching options. `environment` and `release` fall back to `LANGFUSE_TRACING_ENVIRONMENT` and `LANGFUSE_RELEASE`, and only fill spans that do not carry them already.
21
+ */
22
+ constructor(langfuse) {
23
+ super("langfuse", {
24
+ ...langfuse,
25
+ maxBufferSize: langfuse.maxBufferSize ?? DEFAULT_FLUSH_AT,
26
+ });
27
+ this.langfuse = langfuse;
28
+ this.authorization = `Basic ${Buffer.from(`${langfuse.publicKey}:${langfuse.secretKey}`).toString("base64")}`;
29
+ this.baseUrl = (langfuse.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
30
+ this.policy = {
31
+ environment: langfuse.environment ?? process.env.LANGFUSE_TRACING_ENVIRONMENT,
32
+ release: langfuse.release ?? process.env.LANGFUSE_RELEASE,
33
+ redactIO: langfuse.redactIO ?? false,
34
+ mask: langfuse.mask,
35
+ };
25
36
  }
26
37
  async initialize() {
27
38
  if (this.initialized) {
28
39
  return;
29
40
  }
30
- // Verify credentials with a simple check
31
- if (!this.publicKey || !this.secretKey) {
41
+ if (!this.langfuse.publicKey || !this.langfuse.secretKey) {
32
42
  throw new Error("Langfuse publicKey and secretKey are required");
33
43
  }
34
44
  this.initialized = true;
35
- this.startFlushInterval(this.config.flushIntervalMs ?? 5000);
45
+ this.startFlushInterval(this.config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
46
+ this.flushInterval?.unref();
36
47
  }
37
48
  async exportSpan(span) {
38
- const startTime = Date.now();
39
- try {
40
- // Create trace if this is a root span
41
- if (!span.parentSpanId) {
42
- await this.createTrace(span);
43
- }
44
- // Create span/generation based on type
45
- if (span.type === SpanType.MODEL_GENERATION) {
46
- await this.createGeneration(span);
47
- }
48
- else {
49
- await this.createSpan(span);
50
- }
51
- return this.createSuccessResult(1, Date.now() - startTime);
52
- }
53
- catch (error) {
54
- return this.createFailureResult([span.spanId], error instanceof Error ? error.message : String(error), Date.now() - startTime);
55
- }
49
+ this.bufferSpan(span);
50
+ return this.createSuccessResult(1, 0);
56
51
  }
57
52
  async exportBatch(spans) {
58
53
  const startTime = Date.now();
59
- const results = await Promise.allSettled(spans.map((s) => this.exportSpan(s)));
60
- const successful = results.filter((r) => r.status === "fulfilled" && r.value.success).length;
61
- const failed = spans.length - successful;
62
- return {
63
- success: failed === 0,
64
- exportedCount: successful,
65
- failedCount: failed,
66
- errors: results
67
- .filter((r) => r.status === "rejected" ||
68
- (r.status === "fulfilled" && !r.value.success))
69
- .map((r, i) => ({
70
- spanId: spans[i].spanId,
71
- error: r.status === "rejected" ? String(r.reason) : "Export failed",
72
- retryable: true,
73
- })),
74
- durationMs: Date.now() - startTime,
75
- };
54
+ await this.deliver(spans.map((span) => toOtlpSpan(span, this.policy)));
55
+ return this.createSuccessResult(spans.length, Date.now() - startTime);
76
56
  }
77
57
  async flush() {
78
- if (this.buffer.length > 0) {
79
- const spans = [...this.buffer];
80
- this.buffer = [];
58
+ const spans = this.buffer;
59
+ this.buffer = [];
60
+ if (spans.length > 0) {
61
+ const pending = this.exportQuietly(spans);
62
+ this.inflight.add(pending);
63
+ void pending.finally(() => this.inflight.delete(pending));
64
+ }
65
+ await Promise.all(this.inflight);
66
+ }
67
+ async exportQuietly(spans) {
68
+ try {
81
69
  await this.exportBatch(spans);
82
70
  }
71
+ catch (error) {
72
+ logger.warn("[langfuse] Export failed", {
73
+ error: error instanceof Error ? error.message : String(error),
74
+ });
75
+ }
83
76
  }
84
77
  async shutdown() {
85
78
  await this.flush();
@@ -95,115 +88,69 @@ export class LangfuseExporter extends BaseExporter {
95
88
  return this.createHealthStatus(false, ["Health check failed"]);
96
89
  }
97
90
  }
98
- /**
99
- * Verify connectivity to Langfuse API
100
- */
101
91
  async ping() {
102
- const credentials = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64");
103
- const response = await fetch(`${this.baseUrl}/api/public/health`, {
104
- method: "GET",
105
- headers: {
106
- Authorization: `Basic ${credentials}`,
107
- },
92
+ const response = await fetch(`${this.baseUrl}${HEALTH_PATH}`, {
93
+ headers: { Authorization: this.authorization },
94
+ signal: AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS),
108
95
  });
109
- if (!response.ok && response.status !== 404) {
110
- // 404 is acceptable as health endpoint may not exist
96
+ if (!response.ok) {
111
97
  throw new Error(`Langfuse API unreachable: ${response.status}`);
112
98
  }
113
99
  }
114
- /**
115
- * Create a Langfuse trace
116
- */
117
- async createTrace(span) {
118
- const body = {
119
- id: span.traceId,
120
- name: span.name,
121
- userId: span.attributes["user.id"],
122
- sessionId: span.attributes["session.id"],
123
- // Only pick safe, non-PII attributes for metadata — intentionally excludes
124
- // input, output, error.stack, and other user content to match Braintrust exporter
125
- metadata: filterSafeMetadata(span.attributes),
126
- release: this.release,
127
- tags: this.extractTags(span),
128
- };
129
- await this.apiCall("/api/public/traces", body);
130
- }
131
- /**
132
- * Create a Langfuse span
133
- */
134
- async createSpan(span) {
135
- const langfuseSpan = SpanSerializer.toLangfuseFormat(span);
136
- const body = {
137
- ...langfuseSpan,
138
- traceId: span.traceId,
139
- };
140
- if (this.redactIO) {
141
- delete body["input"];
142
- delete body["output"];
100
+ async deliver(spans) {
101
+ for (const chunk of chunkOtlpSpans(spans)) {
102
+ try {
103
+ await this.withRetry(() => this.post(chunk), "export");
104
+ }
105
+ catch (error) {
106
+ logger.warn(`[langfuse] Dropped ${chunk.length} spans`, {
107
+ error: error instanceof Error ? error.message : String(error),
108
+ });
109
+ }
143
110
  }
144
- await this.apiCall("/api/public/spans", body);
145
111
  }
146
- /**
147
- * Create a Langfuse generation (for LLM calls)
148
- */
149
- async createGeneration(span) {
150
- const langfuseSpan = SpanSerializer.toLangfuseFormat(span);
151
- const body = {
152
- traceId: span.traceId,
153
- id: langfuseSpan.id,
154
- parentObservationId: langfuseSpan.parentObservationId,
155
- name: langfuseSpan.name,
156
- startTime: langfuseSpan.startTime,
157
- endTime: langfuseSpan.endTime,
158
- model: span.attributes["ai.model"],
159
- modelParameters: {
160
- temperature: span.attributes["ai.temperature"],
161
- maxTokens: span.attributes["ai.max_tokens"],
162
- topP: span.attributes["ai.top_p"],
163
- },
164
- input: this.redactIO ? undefined : langfuseSpan.input,
165
- output: this.redactIO ? undefined : langfuseSpan.output,
166
- usage: langfuseSpan.usage,
167
- metadata: langfuseSpan.metadata,
168
- level: langfuseSpan.level,
169
- statusMessage: langfuseSpan.statusMessage,
170
- };
171
- await this.apiCall("/api/public/generations", body);
172
- }
173
- /**
174
- * Make API call to Langfuse
175
- */
176
- async apiCall(path, body) {
177
- const credentials = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64");
178
- const response = await fetch(`${this.baseUrl}${path}`, {
112
+ async post(spans) {
113
+ const response = await fetch(`${this.baseUrl}${TRACES_PATH}`, {
179
114
  method: "POST",
180
115
  headers: {
181
116
  "Content-Type": "application/json",
182
- Authorization: `Basic ${credentials}`,
117
+ Authorization: this.authorization,
118
+ "x-langfuse-sdk-name": "javascript",
119
+ "x-langfuse-ingestion-version": "4",
120
+ "x-langfuse-public-key": this.langfuse.publicKey,
183
121
  },
184
- body: JSON.stringify(body),
122
+ body: JSON.stringify({
123
+ resourceSpans: [
124
+ {
125
+ resource: {
126
+ attributes: toOtlpAttributes({ "service.name": "service" }),
127
+ },
128
+ scopeSpans: [{ scope: { name: "sdk" }, spans }],
129
+ },
130
+ ],
131
+ }),
132
+ signal: AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS),
185
133
  });
186
- if (!response.ok) {
187
- const text = await response.text();
188
- throw new Error(`Langfuse API error: ${response.status} - ${text}`);
134
+ if (response.status === 401 || response.status === 403) {
135
+ if (!credentialsWarned) {
136
+ credentialsWarned = true;
137
+ logger.warn("[langfuse] Langfuse credentials rejected");
138
+ }
139
+ return;
189
140
  }
190
- }
191
- /**
192
- * Extract tags from span attributes
193
- */
194
- extractTags(span) {
195
- const tags = [];
196
- if (span.attributes["ai.provider"]) {
197
- tags.push(`provider:${span.attributes["ai.provider"]}`);
141
+ if (isRetryable(response.status)) {
142
+ throw new Error(`Langfuse API error: ${response.status}`);
198
143
  }
199
- if (span.attributes["ai.model"]) {
200
- tags.push(`model:${span.attributes["ai.model"]}`);
144
+ if (!response.ok) {
145
+ logger.warn(`[langfuse] Export rejected with ${response.status}`);
146
+ return;
201
147
  }
202
- if (span.attributes["deployment.environment"]) {
203
- tags.push(`env:${span.attributes["deployment.environment"]}`);
148
+ const body = (await response.json().catch(() => ({})));
149
+ const rejected = Number(body.partialSuccess?.rejectedSpans ?? 0);
150
+ if (rejected > 0) {
151
+ logger.warn(`[langfuse] ${rejected} spans rejected`, {
152
+ message: body.partialSuccess?.errorMessage,
153
+ });
204
154
  }
205
- return tags;
206
155
  }
207
156
  }
208
- // Safe metadata filtering imported from shared module to avoid duplication
209
- import { filterSafeMetadata } from "../utils/safeMetadata.js";
@@ -3,6 +3,7 @@
3
3
  * Multi-platform observability with OpenTelemetry integration
4
4
  */
5
5
  export { ExporterRegistry, getExporterRegistry, resetExporterRegistry, } from "./exporterRegistry.js";
6
+ export { createExporterSpanProcessor, ExporterSpanProcessor, } from "./exporterSpanProcessor.js";
6
7
  export { ArizeExporter } from "./exporters/arizeExporter.js";
7
8
  export { BaseExporter, NoOpExporter } from "./exporters/baseExporter.js";
8
9
  export { BraintrustExporter } from "./exporters/braintrustExporter.js";
@@ -4,6 +4,7 @@
4
4
  */
5
5
  // Core - Registry with singleton pattern
6
6
  export { ExporterRegistry, getExporterRegistry, resetExporterRegistry, } from "./exporterRegistry.js";
7
+ export { createExporterSpanProcessor, ExporterSpanProcessor, } from "./exporterSpanProcessor.js";
7
8
  export { ArizeExporter } from "./exporters/arizeExporter.js";
8
9
  // Exporters
9
10
  export { BaseExporter, NoOpExporter } from "./exporters/baseExporter.js";
@@ -0,0 +1,8 @@
1
+ import type { LangfuseAttributePolicy, SpanAttributes } from "../types/index.js";
2
+ /**
3
+ * @description Applies the exporter's policy to a span's attributes: fills environment and release when the span has none, removes input and output when `redactIO` is set, and masks input, output and metadata when a mask is configured.
4
+ * @param attributes {SpanAttributes} - Attributes of the span being exported.
5
+ * @param policy {LangfuseAttributePolicy} - Environment, release, redaction and mask settings.
6
+ * @returns {SpanAttributes} A new attribute record; the input is not modified.
7
+ */
8
+ export declare function protectAttributes(attributes: SpanAttributes, policy: LangfuseAttributePolicy): SpanAttributes;
@@ -0,0 +1,47 @@
1
+ import { CONTENT_KEYS, FULLY_MASKED, METADATA_PREFIXES, } from "../constants/langfuse.js";
2
+ function parseContent(value) {
3
+ const text = typeof value === "string" ? value : JSON.stringify(value);
4
+ try {
5
+ return JSON.parse(text);
6
+ }
7
+ catch {
8
+ return text ?? null;
9
+ }
10
+ }
11
+ function maskValue(value, mask) {
12
+ try {
13
+ const masked = mask?.({ data: parseContent(value) });
14
+ return typeof masked === "string" ? masked : JSON.stringify(masked);
15
+ }
16
+ catch {
17
+ return FULLY_MASKED;
18
+ }
19
+ }
20
+ /**
21
+ * @description Applies the exporter's policy to a span's attributes: fills environment and release when the span has none, removes input and output when `redactIO` is set, and masks input, output and metadata when a mask is configured.
22
+ * @param attributes {SpanAttributes} - Attributes of the span being exported.
23
+ * @param policy {LangfuseAttributePolicy} - Environment, release, redaction and mask settings.
24
+ * @returns {SpanAttributes} A new attribute record; the input is not modified.
25
+ */
26
+ export function protectAttributes(attributes, policy) {
27
+ const result = {
28
+ ...(policy.environment && { "langfuse.environment": policy.environment }),
29
+ ...(policy.release && { "langfuse.release": policy.release }),
30
+ ...attributes,
31
+ };
32
+ for (const key of Object.keys(result)) {
33
+ if (CONTENT_KEYS.has(key)) {
34
+ if (policy.redactIO) {
35
+ delete result[key];
36
+ }
37
+ else if (policy.mask) {
38
+ result[key] = maskValue(result[key], policy.mask);
39
+ }
40
+ }
41
+ else if (policy.mask &&
42
+ METADATA_PREFIXES.some((prefix) => key.startsWith(prefix))) {
43
+ result[key] = maskValue(result[key], policy.mask);
44
+ }
45
+ }
46
+ return result;
47
+ }