Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/langchain-run-name-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@posthog/ai': patch
---

Fix LangChain spans being named after their class instead of the runnable. LangChain passes `runName` as a bare string, which the name resolver skipped because it only inspected object arguments, so every tool span was captured as `DynamicStructuredTool` rather than the tool's own name.
15 changes: 11 additions & 4 deletions packages/ai/src/langchain/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,17 @@ export class LangChainCallbackHandler extends BaseCallbackHandler {
private _getLangchainRunName(serialized: any, ...args: any): string | undefined {
if (args && args.length > 0) {
for (const arg of args) {
if (arg && typeof arg === 'object' && 'name' in arg) {
return arg.name
} else if (arg && typeof arg === 'object' && 'runName' in arg) {
return arg.runName
// LangChain hands runName through as a bare string, not wrapped in an object
if (typeof arg === 'string' && arg) {
return arg
}
if (arg && typeof arg === 'object') {
if (arg.name) {
return arg.name
}
if (arg.runName) {
return arg.runName
}
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions packages/ai/tests/callbacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,29 @@ describe('LangChainCallbackHandler', () => {
})
})

describe('LangChainCallbackHandler span naming', () => {
it('names a tool span from runName rather than the serialized class', () => {
const handler = new LangChainCallbackHandler({ client: mockPostHogClient })
jest.clearAllMocks()

const serialized = {
lc: 1,
type: 'constructor' as const,
id: ['langchain', 'tools', 'DynamicStructuredTool'],
kwargs: {},
}
const runId = 'run_tool_name'

// LangChain calls this with (tool, input, runId, parentRunId, tags, metadata, runName)
handler.handleToolStart(serialized, '{"city":"Paris"}', runId, 'parent_run', [], {}, 'get_weather')
handler.handleToolEnd('sunny', runId, 'parent_run')

const [captureCall] = (mockPostHogClient.capture as jest.Mock).mock.calls
expect(captureCall[0].event).toBe('$ai_span')
expect(captureCall[0].properties['$ai_span_name']).toBe('get_weather')
})
})

describe('LangChainCallbackHandler trace/span state sanitization', () => {
it('redacts base64 data URLs from $ai_input_state and $ai_output_state', () => {
const handler = new LangChainCallbackHandler({ client: mockPostHogClient })
Expand Down