add langsmith tracing initialized from environment variables (#6271)

This commit is contained in:
mmattu-wd
2026-04-28 17:41:09 -07:00
committed by GitHub
parent da8b251a9a
commit a3ffe6611b
6 changed files with 528 additions and 20 deletions
+104 -1
View File
@@ -1,5 +1,6 @@
import { OTLPTraceExporter as ProtoOTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { getPhoenixTracer } from './handler'
import { getPhoenixTracer, AnalyticHandler } from './handler'
import { resetTracingEnvCache } from './tracingEnv'
jest.mock('@opentelemetry/exporter-trace-otlp-proto', () => {
return {
@@ -9,6 +10,42 @@ jest.mock('@opentelemetry/exporter-trace-otlp-proto', () => {
}
})
// Track every RunTree constructed so tests can assert per-instance lifecycle calls.
// Prefixed with `mock` so Jest allows referencing it from inside the mock factory.
const mockRunTreeInstances: Array<{
id: string
config: any
postRun: jest.Mock
patchRun: jest.Mock
end: jest.Mock
createChild: jest.Mock
}> = []
jest.mock('langsmith', () => {
let counter = 0
class MockRunTree {
id: string
config: any
postRun: jest.Mock
patchRun: jest.Mock
end: jest.Mock
createChild: jest.Mock
constructor(config: any) {
this.config = config
this.id = `run-${++counter}`
this.postRun = jest.fn().mockResolvedValue(undefined)
this.patchRun = jest.fn().mockResolvedValue(undefined)
this.end = jest.fn().mockResolvedValue(undefined)
this.createChild = jest.fn().mockImplementation(async (childCfg: any) => new MockRunTree(childCfg))
mockRunTreeInstances.push(this as any)
}
}
return {
Client: jest.fn().mockImplementation(() => ({})),
RunTree: MockRunTree
}
})
describe('URL Handling For Phoenix Tracer', () => {
const apiKey = 'test-api-key'
const projectName = 'test-project-name'
@@ -331,3 +368,69 @@ describe('onLLMEnd Usage Metadata Extraction Logic', () => {
})
})
})
/**
* Regression coverage for the chainRun map clobbering bug.
*
* AnalyticHandler is a per-chatId singleton, so recursive executeAgentFlow calls (e.g. an
* iterationAgentflow sub-flow) re-enter `onChainStart` on the same instance. The previous
* implementation replaced `this.handlers['langSmith'].chainRun` wholesale on every call, which
* dropped the outer call's parent RunTree. When control returned to the outer flow, its
* `onChainEnd` lookup missed and the outer run was never `patchRun`-ed — leaving an open run in
* LangSmith and breaking context propagation for any subsequent nodes.
*/
describe('AnalyticHandler chainRun map on recursive onChainStart', () => {
const LANGSMITH_KEYS = ['LANGSMITH_TRACING', 'LANGSMITH_API_KEY', 'LANGSMITH_ENDPOINT', 'LANGSMITH_PROJECT'] as const
let envSnapshot: Partial<Record<(typeof LANGSMITH_KEYS)[number], string | undefined>>
beforeEach(() => {
envSnapshot = {}
for (const k of LANGSMITH_KEYS) envSnapshot[k] = process.env[k]
for (const k of LANGSMITH_KEYS) delete process.env[k]
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'test-key'
resetTracingEnvCache()
mockRunTreeInstances.length = 0
})
afterEach(() => {
for (const k of LANGSMITH_KEYS) delete process.env[k]
for (const k of LANGSMITH_KEYS) {
const v = envSnapshot[k]
if (v !== undefined) process.env[k] = v
}
resetTracingEnvCache()
AnalyticHandler.resetInstance('recursive-chain-test')
})
it('preserves the outer parent RunTree when a nested onChainStart runs on the same instance', async () => {
const chatId = 'recursive-chain-test'
const handler = AnalyticHandler.getInstance({ inputs: {} } as any, { chatId })
await handler.init()
// Outer flow's onChainStart — simulates the first executeAgentFlow call.
const outerIds = await handler.onChainStart('OuterFlow', 'outer input')
expect(outerIds.langSmith.chainRun).toBeDefined()
// Recursive executeAgentFlow (iterationAgentflow) re-enters on the same singleton.
// No parentIds passed, matching the real call site in buildAgentflow.ts.
const innerIds = await handler.onChainStart('InnerFlow', 'inner input')
expect(innerIds.langSmith.chainRun).toBeDefined()
expect(innerIds.langSmith.chainRun).not.toBe(outerIds.langSmith.chainRun)
// Inner flow completes first.
await handler.onChainEnd(innerIds, 'inner output')
// Outer flow must still be able to end its own run.
await handler.onChainEnd(outerIds, 'outer output', true)
// Both the outer and inner RunTree instances should have been patched exactly once —
// before the fix, the outer's patchRun was skipped because the map entry was overwritten.
expect(mockRunTreeInstances).toHaveLength(2)
const [outerRun, innerRun] = mockRunTreeInstances
expect(outerRun.patchRun).toHaveBeenCalledTimes(1)
expect(innerRun.patchRun).toHaveBeenCalledTimes(1)
expect(outerRun.end).toHaveBeenCalledWith({ outputs: { output: 'outer output' } })
expect(innerRun.end).toHaveBeenCalledWith({ outputs: { output: 'inner output' } })
})
})
+33 -12
View File
@@ -16,7 +16,6 @@ import { Resource } from '@opentelemetry/resources'
import { SimpleSpanProcessor, Tracer } from '@opentelemetry/sdk-trace-base'
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
import { BaseCallbackHandler, NewTokenIndices, HandleLLMNewTokenCallbackFields } from '@langchain/core/callbacks/base'
import * as CallbackManagerModule from '@langchain/core/callbacks/manager'
import { LangChainTracer, LangChainTracerFields } from '@langchain/core/tracers/tracer_langchain'
@@ -26,6 +25,7 @@ import { AgentAction } from '@langchain/core/agents'
import { LunaryHandler } from '@langchain/community/callbacks/handlers/lunary'
import { getCredentialData, getCredentialParam, getEnvironmentVariable } from './utils'
import { applyEnvTracingProviders, tracingEnvEnabled } from './tracingEnv'
import { EvaluationRunTracer } from '../evaluation/EvaluationRunTracer'
import { EvaluationRunTracerLlama } from '../evaluation/EvaluationRunTracerLlama'
import { ICommonObject, IDatabaseEntity, INodeData, IServerSideEventStreamer } from './Interface'
@@ -515,16 +515,17 @@ class ExtendedLunaryHandler extends LunaryHandler {
export const additionalCallbacks = async (nodeData: INodeData, options: ICommonObject) => {
try {
if (!options.analytic) return []
if (!options.analytic && !tracingEnvEnabled()) return []
const analytic = JSON.parse(options.analytic)
const initial = options.analytic ? JSON.parse(options.analytic) : {}
const { analytic, envCredentials } = applyEnvTracingProviders(initial)
const callbacks: any = []
for (const provider in analytic) {
const providerStatus = analytic[provider].status as boolean
if (providerStatus) {
const credentialId = analytic[provider].credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', options)
const credentialData =
envCredentials[provider] ?? (await getCredentialData((analytic[provider].credentialId as string) ?? '', options))
if (provider === 'langSmith') {
const langSmithProject = analytic[provider].projectName as string
@@ -774,23 +775,31 @@ export class AnalyticHandler {
if (this.initialized) return
try {
if (!this.options.analytic) return
const hasAnalyticsConfig = Boolean(this.options.analytic)
if (!hasAnalyticsConfig && !tracingEnvEnabled()) return
const analytic = JSON.parse(this.options.analytic)
const initial = hasAnalyticsConfig ? JSON.parse(this.options.analytic) : {}
const { analytic, envCredentials } = applyEnvTracingProviders(initial)
for (const provider in analytic) {
const providerStatus = analytic[provider].status as boolean
if (providerStatus) {
const credentialId = analytic[provider].credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', this.options)
const credentialData =
envCredentials[provider] ??
(await getCredentialData((analytic[provider].credentialId as string) ?? '', this.options))
await this.initializeProvider(provider, analytic[provider], credentialData)
}
}
this.initialized = true
} catch (e) {
throw new Error(e)
}
}
hasActiveProviders(): boolean {
return Object.keys(this.handlers).length > 0
}
// Add getter for handlers (useful for debugging)
getHandlers(): ICommonObject {
return this.handlers
@@ -807,7 +816,10 @@ export class AnalyticHandler {
apiKey: langSmithApiKey
})
this.handlers['langSmith'] = { client, langSmithProject }
this.handlers['langSmith'] = {
client,
langSmithProject
}
} else if (provider === 'langFuse') {
const release = providerConfig.release as string
const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData)
@@ -927,7 +939,13 @@ export class AnalyticHandler {
}
const parentRun = new RunTree(parentRunConfig)
await parentRun.postRun()
this.handlers['langSmith'].chainRun = { [parentRun.id]: parentRun }
// Merge rather than replace: AnalyticHandler is a per-chatId singleton, so recursive
// executeAgentFlow calls (e.g. iterationAgentflow) re-enter onChainStart on the same
// instance. Replacing would drop the outer call's parent entry and orphan its trace.
this.handlers['langSmith'].chainRun = {
...this.handlers['langSmith'].chainRun,
[parentRun.id]: parentRun
}
returnIds['langSmith'].chainRun = parentRun.id
} else {
const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun]
@@ -940,7 +958,10 @@ export class AnalyticHandler {
}
})
await childChainRun.postRun()
this.handlers['langSmith'].chainRun = { [childChainRun.id]: childChainRun }
this.handlers['langSmith'].chainRun = {
...this.handlers['langSmith'].chainRun,
[childChainRun.id]: childChainRun
}
returnIds['langSmith'].chainRun = childChainRun.id
}
}
+1
View File
@@ -11,6 +11,7 @@ export * from './textToSpeech'
export * from './storageUtils'
export * from './storage'
export * from './handler'
export { tracingEnvEnabled } from './tracingEnv'
export * from '../evaluation/EvaluationRunner'
export * from './followUpPrompts'
export * from './validator'
+269
View File
@@ -0,0 +1,269 @@
import {
getLangSmithEnvConfig,
resetTracingEnvCache,
TRACING_ENV_PROVIDERS,
tracingEnvEnabled,
applyEnvTracingProviders
} from './tracingEnv'
const ENV_KEYS = [
'LANGSMITH_TRACING',
'LANGCHAIN_TRACING_V2',
'LANGSMITH_API_KEY',
'LANGCHAIN_API_KEY',
'LANGSMITH_ENDPOINT',
'LANGCHAIN_ENDPOINT',
'LANGSMITH_PROJECT',
'LANGCHAIN_PROJECT'
] as const
const clearEnv = () => {
for (const k of ENV_KEYS) delete process.env[k]
}
let envSnapshot: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>>
beforeEach(() => {
envSnapshot = {}
for (const k of ENV_KEYS) envSnapshot[k] = process.env[k]
clearEnv()
resetTracingEnvCache()
})
afterEach(() => {
clearEnv()
for (const k of ENV_KEYS) {
const v = envSnapshot[k]
if (v !== undefined) process.env[k] = v
}
resetTracingEnvCache()
})
describe('getLangSmithEnvConfig', () => {
it('returns undefined when no tracing flag is set', () => {
expect(getLangSmithEnvConfig()).toBeUndefined()
})
it("returns undefined when tracing flag is not exactly 'true'", () => {
process.env.LANGSMITH_TRACING = 'false'
process.env.LANGSMITH_API_KEY = 'k'
expect(getLangSmithEnvConfig()).toBeUndefined()
process.env.LANGSMITH_TRACING = '1'
expect(getLangSmithEnvConfig()).toBeUndefined()
process.env.LANGSMITH_TRACING = 'TRUE'
expect(getLangSmithEnvConfig()).toBeUndefined()
})
it('returns undefined when tracing flag is true but no api key is set', () => {
process.env.LANGSMITH_TRACING = 'true'
expect(getLangSmithEnvConfig()).toBeUndefined()
})
it('returns config with only apiKey when flag + new key are set', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'new-key'
expect(getLangSmithEnvConfig()).toEqual({
apiKey: 'new-key',
endpoint: undefined,
projectName: undefined
})
})
it('honors legacy LANGCHAIN_* vars alone', () => {
process.env.LANGCHAIN_TRACING_V2 = 'true'
process.env.LANGCHAIN_API_KEY = 'legacy-key'
process.env.LANGCHAIN_ENDPOINT = 'https://legacy.example.com'
process.env.LANGCHAIN_PROJECT = 'legacy-proj'
expect(getLangSmithEnvConfig()).toEqual({
apiKey: 'legacy-key',
endpoint: 'https://legacy.example.com',
projectName: 'legacy-proj'
})
})
it('prefers new LANGSMITH_* vars over legacy LANGCHAIN_* when both are set', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGCHAIN_TRACING_V2 = 'true'
process.env.LANGSMITH_API_KEY = 'new-key'
process.env.LANGCHAIN_API_KEY = 'legacy-key'
process.env.LANGSMITH_ENDPOINT = 'https://new.example.com'
process.env.LANGCHAIN_ENDPOINT = 'https://legacy.example.com'
process.env.LANGSMITH_PROJECT = 'new-proj'
process.env.LANGCHAIN_PROJECT = 'legacy-proj'
expect(getLangSmithEnvConfig()).toEqual({
apiKey: 'new-key',
endpoint: 'https://new.example.com',
projectName: 'new-proj'
})
})
it('falls back to legacy api key when only new tracing flag is set without new api key', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGCHAIN_API_KEY = 'legacy-key'
expect(getLangSmithEnvConfig()).toEqual({
apiKey: 'legacy-key',
endpoint: undefined,
projectName: undefined
})
})
it('includes endpoint and projectName when provided via new vars', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k'
process.env.LANGSMITH_ENDPOINT = 'https://api.smith.langchain.com'
process.env.LANGSMITH_PROJECT = 'my-proj'
expect(getLangSmithEnvConfig()).toEqual({
apiKey: 'k',
endpoint: 'https://api.smith.langchain.com',
projectName: 'my-proj'
})
})
})
describe('memoized getEnvConfig via TRACING_ENV_PROVIDERS', () => {
const langSmith = TRACING_ENV_PROVIDERS.find((p) => p.name === 'langSmith')!
it('caches the resolved config across calls', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k1'
const first = langSmith.getEnvConfig()
expect(first).toMatchObject({ apiKey: 'k1' })
// Mutating env after cache should not affect subsequent reads.
process.env.LANGSMITH_API_KEY = 'k2'
const second = langSmith.getEnvConfig()
expect(second).toMatchObject({ apiKey: 'k1' })
})
it('caches undefined results too', () => {
expect(langSmith.getEnvConfig()).toBeUndefined()
// Even after enabling env, the cached undefined sticks until reset.
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k'
expect(langSmith.getEnvConfig()).toBeUndefined()
})
it('resetTracingEnvCache forces re-read of env on the next call', () => {
expect(langSmith.getEnvConfig()).toBeUndefined()
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k'
resetTracingEnvCache()
expect(langSmith.getEnvConfig()).toMatchObject({ apiKey: 'k' })
clearEnv()
resetTracingEnvCache()
expect(langSmith.getEnvConfig()).toBeUndefined()
})
})
describe('TRACING_ENV_PROVIDERS langSmith.buildProviderEntry', () => {
const langSmith = TRACING_ENV_PROVIDERS.find((p) => p.name === 'langSmith')!
it('returns full provider entry when endpoint and projectName are present', () => {
const entry = langSmith.buildProviderEntry({
apiKey: 'k',
endpoint: 'https://e.example.com',
projectName: 'proj-x'
})
expect(entry).toEqual({
providerConfig: { projectName: 'proj-x', status: true },
credentialData: {
langSmithApiKey: 'k',
langSmithEndpoint: 'https://e.example.com'
}
})
})
it("defaults projectName to 'default' and omits langSmithEndpoint when endpoint is missing", () => {
const entry = langSmith.buildProviderEntry({ apiKey: 'k' })
expect(entry).toEqual({
providerConfig: { projectName: 'default', status: true },
credentialData: { langSmithApiKey: 'k' }
})
expect(entry.credentialData).not.toHaveProperty('langSmithEndpoint')
})
})
describe('tracingEnvEnabled', () => {
it('returns false when no tracing env is configured', () => {
expect(tracingEnvEnabled()).toBe(false)
})
it('returns true when LangSmith tracing env is configured', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k'
expect(tracingEnvEnabled()).toBe(true)
})
})
describe('applyEnvTracingProviders', () => {
it('returns the analytic map unchanged with empty envCredentials when no env is set', () => {
const analytic = { other: { status: true } }
const result = applyEnvTracingProviders(analytic)
expect(result.analytic).toEqual({ other: { status: true } })
expect(result.envCredentials).toEqual({})
})
it('injects provider config and credentials when env is set and analytic has no entry', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k'
process.env.LANGSMITH_ENDPOINT = 'https://e.example.com'
process.env.LANGSMITH_PROJECT = 'proj'
const analytic: Record<string, any> = {}
const result = applyEnvTracingProviders(analytic)
expect(result.analytic.langSmith).toEqual({
projectName: 'proj',
status: true
})
expect(result.envCredentials).toEqual({
langSmith: {
langSmithApiKey: 'k',
langSmithEndpoint: 'https://e.example.com'
}
})
})
it('skips env injection when UI analytic entry already has status === true', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'env-key'
const uiEntry = { projectName: 'ui-proj', status: true }
const analytic: Record<string, any> = { langSmith: uiEntry }
const result = applyEnvTracingProviders(analytic)
expect(result.analytic.langSmith).toBe(uiEntry)
expect(result.envCredentials).toEqual({})
})
it('applies env when UI entry exists but status is not true', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'env-key'
const analytic: Record<string, any> = { langSmith: { projectName: 'ui-proj', status: false } }
const result = applyEnvTracingProviders(analytic)
expect(result.analytic.langSmith).toEqual({
projectName: 'default',
status: true
})
expect(result.envCredentials.langSmith).toEqual({ langSmithApiKey: 'env-key' })
})
it('does not mutate the passed-in analytic object', () => {
process.env.LANGSMITH_TRACING = 'true'
process.env.LANGSMITH_API_KEY = 'k'
const analytic: Record<string, any> = {}
const result = applyEnvTracingProviders(analytic)
expect(result.analytic).not.toBe(analytic)
expect(analytic.langSmith).toBeUndefined()
expect(result.analytic.langSmith).toBeDefined()
})
})
+111
View File
@@ -0,0 +1,111 @@
import { getEnvironmentVariable } from './utils'
import { ICommonObject } from './Interface'
export interface TracingEnvProvider {
name: string
getEnvConfig: () => ICommonObject | undefined
/**
* Map env config to the `(providerConfig, credentialData)` pair the UI-driven analytics loop
* consumes, so env-var and UI sources flow through the same code path — no duplicate client
* construction.
*/
buildProviderEntry: (cfg: ICommonObject) => { providerConfig: ICommonObject; credentialData: ICommonObject }
}
/**
* Process-lifetime cache for env-var tracing configs.
*
* Entries are populated on first access and never expire — a server restart is
* required to pick up env-var changes. This is intentional: env vars are set at
* deploy time and re-reading them on every agent execution would add overhead
* with no benefit. Use {@link resetTracingEnvCache} (test-only) to clear the
* cache in unit tests.
*/
const tracingEnvConfigCache = new Map<string, { value: ICommonObject | undefined }>()
const memoizeEnvConfig = (name: string, resolver: () => ICommonObject | undefined): (() => ICommonObject | undefined) => {
return () => {
const cached = tracingEnvConfigCache.get(name)
if (cached) return cached.value
const value = resolver()
tracingEnvConfigCache.set(name, { value })
return value
}
}
/** @internal Test-only: drop cached env-var tracing configs so a subsequent call re-reads env. */
export const resetTracingEnvCache = (): void => {
tracingEnvConfigCache.clear()
}
/**
* Env var flags that activate LangChain's built-in auto-tracer (see @langchain/core
* `isTracingEnabled()`). Once Flowise adopts the tracing config, these must be cleared from
* `process.env` so the auto-tracer doesn't emit duplicate top-level runs alongside Flowise's
* manual `onLLMStart`/`onToolStart` child RunTrees.
*/
const LANGCHAIN_TRACING_FLAG_VARS = ['LANGSMITH_TRACING', 'LANGCHAIN_TRACING_V2', 'LANGSMITH_TRACING_V2', 'LANGCHAIN_TRACING'] as const
/**
* Reads LangSmith env vars (both new `LANGSMITH_*` and legacy `LANGCHAIN_*` prefixes).
* Returns a config object if tracing is enabled and an API key is present; otherwise undefined.
*
* Side effect: on a successful read, the four LangChain tracing-flag env vars are deleted from
* `process.env`. Flowise owns tracing emission from this point on; leaving the flags set would let
* LangChain's auto-tracer fire on every `.invoke()`/`.call()` and produce orphan top-level runs
* next to the manually-emitted parent/child RunTree.
*/
export const getLangSmithEnvConfig = (): { apiKey: string; endpoint?: string; projectName?: string } | undefined => {
const tracingFlag = getEnvironmentVariable('LANGSMITH_TRACING') ?? getEnvironmentVariable('LANGCHAIN_TRACING_V2')
if (tracingFlag !== 'true') return undefined
const apiKey = getEnvironmentVariable('LANGSMITH_API_KEY') ?? getEnvironmentVariable('LANGCHAIN_API_KEY')
if (!apiKey) return undefined
const endpoint = getEnvironmentVariable('LANGSMITH_ENDPOINT') ?? getEnvironmentVariable('LANGCHAIN_ENDPOINT')
const projectName = getEnvironmentVariable('LANGSMITH_PROJECT') ?? getEnvironmentVariable('LANGCHAIN_PROJECT')
// the four LangChain tracing-flag env vars are deleted from `process.env`. Flowise owns tracing
// emission from this point on; leaving the flags set would letLangChain's auto-tracer fire on
// every `.invoke()`/`.call()` and produce orphan top-level runs next to the manually-emitted
// parent/child RunTree.
for (const k of LANGCHAIN_TRACING_FLAG_VARS) delete process.env[k]
return { apiKey, endpoint, projectName }
}
export const TRACING_ENV_PROVIDERS: TracingEnvProvider[] = [
{
name: 'langSmith',
getEnvConfig: memoizeEnvConfig('langSmith', getLangSmithEnvConfig),
buildProviderEntry: (cfg) => ({
providerConfig: { projectName: cfg.projectName ?? 'default', status: true },
credentialData: {
langSmithApiKey: cfg.apiKey,
...(cfg.endpoint ? { langSmithEndpoint: cfg.endpoint } : {})
}
})
}
]
export const tracingEnvEnabled = (): boolean => TRACING_ENV_PROVIDERS.some((p) => p.getEnvConfig() !== undefined)
/**
* Merge env-var-enabled tracing providers into the analytic map. UI config wins when both sources
* activate the same provider. Returns a credentialData map for env-injected providers so the loop
* can skip DB credential loading for those entries.
*/
export const applyEnvTracingProviders = (
analytic: Record<string, any>
): { analytic: Record<string, any>; envCredentials: Record<string, ICommonObject> } => {
const envCredentials: Record<string, ICommonObject> = {}
const newEntries: Record<string, any> = {}
for (const p of TRACING_ENV_PROVIDERS) {
const cfg = p.getEnvConfig()
if (!cfg) continue
if (analytic[p.name]?.status === true) continue
const { providerConfig, credentialData } = p.buildProviderEntry(cfg)
newEntries[p.name] = providerConfig
envCredentials[p.name] = credentialData
}
return { analytic: { ...analytic, ...newEntries }, envCredentials }
}
+10 -7
View File
@@ -11,7 +11,8 @@ import {
IMessage,
IServerSideEventStreamer,
convertChatHistoryToText,
generateFollowUpPrompts
generateFollowUpPrompts,
tracingEnvEnabled
} from 'flowise-components'
import {
IncomingAgentflowInput,
@@ -1894,7 +1895,7 @@ export const executeAgentFlow = async ({
let parentTraceIds: ICommonObject | undefined
try {
if (chatflow.analytic) {
if (chatflow.analytic || tracingEnvEnabled()) {
// Override config analytics
let analyticInputs: ICommonObject = {}
if (overrideConfig?.analytics && Object.keys(overrideConfig.analytics).length > 0) {
@@ -1912,11 +1913,13 @@ export const executeAgentFlow = async ({
chatId
})
await analyticHandlers.init()
const flowName = chatflow.name || 'Agentflow'
parentTraceIds = await analyticHandlers.onChainStart(
flowName,
form && Object.keys(form).length > 0 ? JSON.stringify(form) : question || ''
)
if (analyticHandlers?.hasActiveProviders()) {
const flowName = chatflow.name || 'Agentflow'
parentTraceIds = await analyticHandlers.onChainStart(
flowName,
form && Object.keys(form).length > 0 ? JSON.stringify(form) : question || ''
)
}
}
} catch (error) {
logger.error(`[server]: Error initializing analytic handlers: ${getErrorMessage(error)}`)