Skip to content

[BUG] [instrumentation-langchain] _configureSync patch drops non-array inheritableHandlers and duplicates TraceloopCallbackHandler #1016

Description

@dev-jpnobrega

Hey everyone,
I encountered an unexpected behavior while working with instrumentation-langchain.

When using @traceloop/instrumentation-langchain] alongside LangChain's [RunnableWithMessageHistory], [CallbackManager._configureSync] causes two issues:

  1. Drops all existing callback handlers when [inheritableHandlers] is a [CallbackManager] instance (not an array).
  2. Duplicates [TraceloopCallbackHandler] on child runnable invocations, resulting in duplicate spans/traces sent to the backend.

Root Cause
In patchCallbackManager, the patched [_configureSync] assumes [inheritableHandlers] is always null or an [Array]:

callbackManagerAny._configureSync = function (inheritableHandlers, localHandlers, ...) {
    const updatedInheritableHandlers = inheritableHandlers && Array.isArray(inheritableHandlers)
        ? [...inheritableHandlers, callbackHandler]
        : [callbackHandler]; // ← BUG: discards CallbackManager instance entirely
    return originalConfigureSync.call(this, updatedInheritableHandlers, localHandlers, ...);
};

However, LangChain internally passes a CallbackManager instance (via runManager.getChild()patchConfig()) as inheritableHandlers. When Array.isArray(CallbackManager) returns false, the patch replaces it with [callbackHandler], discarding all existing handlers — including RootListenersTracer used by RunnableWithMessageHistory to persist chat history messages.

Additionally, the handler is injected unconditionally without checking if a TraceloopCallbackHandler was already inherited from a parent runnable, causing duplicate traces.

Impact

  • Chat history not persisted: RunnableWithMessageHistory._exitHistory is never called because RootListenersTracer is dropped
  • Duplicate telemetry data: Each child runnable invocation adds another TraceloopCallbackHandler, multiplying spans sent to the observability backend

Steps to Reproduce

  1. Initialize traceloop with any exporter:
traceloop.initialize({ appName: 'test', exporter: someExporter });
  1. Create a chain wrapped with RunnableWithMessageHistory:
const chainWithHistory = new RunnableWithMessageHistory({
    runnable: prompt.pipe(llm).pipe(new StringOutputParser()),
    getMessageHistory: (_) => chatHistory,
    inputMessagesKey: 'question',
    historyMessagesKey: 'history',
});
  1. Invoke or stream the chain:
await chainWithHistory.invoke({ question: 'Hello' }, { configurable: { sessionId: 'abc' } });

Expected: Messages are persisted to chat history; single set of traces sent to backend
Actual: Messages are NOT persisted (Bug 1); OR if workaround applied for Bug 1, duplicate traces are sent (Bug 2)

Suggested Fix

In patchCallbackManager, before injecting the handler:

  1. When inheritableHandlers is a CallbackManager instance, extract its inheritableHandlers array instead of discarding it
  2. Filter out any existing TraceloopCallbackHandler (by name === 'traceloop_callback_handler') before adding a new one to prevent duplication
callbackManagerAny._configureSync = function (inheritableHandlers, localHandlers, ...rest) {
    let handlers;
    if (inheritableHandlers && !Array.isArray(inheritableHandlers)) {
        // Extract handlers from CallbackManager instance
        handlers = [...(inheritableHandlers.inheritableHandlers || [])];
    } else {
        handlers = inheritableHandlers ? [...inheritableHandlers] : [];
    }
    // Deduplicate: remove inherited TraceloopCallbackHandler before adding a fresh one
    handlers = handlers.filter(h => h?.name !== 'traceloop_callback_handler');
    handlers.push(callbackHandler);
    return originalConfigureSync.call(this, handlers, localHandlers, ...rest);
};

Environment

  • @traceloop/node-server-sdk: latest
  • @traceloop/instrumentation-langchain: 0.24.0
  • @langchain/core: >=1.0.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions