style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -39,7 +39,11 @@ const mockLangfuseOtelSpanAttributes: Record<string, string> = {
OBSERVATION_USAGE_DETAILS: 'observation.usageDetails',
}
const mockSpanContext = { traceId: 'test-trace-id', spanId: 'test-span-id', traceFlags: 1 }
const mockSpanContext = {
traceId: 'test-trace-id',
spanId: 'test-span-id',
traceFlags: 1,
}
const mockSetAttribute = mock(() => {})
// Child observation mock (returned by rootSpan.startObservation for tools)
@@ -105,13 +109,18 @@ describe('Langfuse integration', () => {
test('replaces home dir in file_path', async () => {
const { sanitizeToolInput } = await import('../sanitize.js')
const home = process.env.HOME ?? '/Users/testuser'
const result = sanitizeToolInput('FileReadTool', { file_path: `${home}/project/file.ts` }) as Record<string, string>
const result = sanitizeToolInput('FileReadTool', {
file_path: `${home}/project/file.ts`,
}) as Record<string, string>
expect(result.file_path).toBe('~/project/file.ts')
})
test('redacts sensitive keys', async () => {
const { sanitizeToolInput } = await import('../sanitize.js')
const result = sanitizeToolInput('MCPTool', { api_key: 'secret123', token: 'abc' }) as Record<string, string>
const result = sanitizeToolInput('MCPTool', {
api_key: 'secret123',
token: 'abc',
}) as Record<string, string>
expect(result.api_key).toBe('[REDACTED]')
expect(result.token).toBe('[REDACTED]')
})
@@ -172,7 +181,9 @@ describe('Langfuse integration', () => {
test('recursively sanitizes nested objects', async () => {
const { sanitizeGlobal } = await import('../sanitize.js')
const result = sanitizeGlobal({ nested: { api_key: 'secret', name: 'test' } }) as Record<string, Record<string, string>>
const result = sanitizeGlobal({
nested: { api_key: 'secret', name: 'test' },
}) as Record<string, Record<string, string>>
expect(result.nested.api_key).toBe('[REDACTED]')
expect(result.nested.name).toBe('test')
})
@@ -313,7 +324,10 @@ describe('Langfuse integration', () => {
// client.js singleton: once processor is set, initLangfuse returns true immediately
// We verify this by checking that calling it multiple times doesn't throw
const { initLangfuse } = await import('../client.js')
expect(() => { initLangfuse(); initLangfuse() }).not.toThrow()
expect(() => {
initLangfuse()
initLangfuse()
}).not.toThrow()
})
})
@@ -330,7 +344,11 @@ describe('Langfuse integration', () => {
describe('createTrace', () => {
test('returns null when langfuse not enabled', async () => {
const { createTrace } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
expect(span).toBeNull()
})
@@ -338,26 +356,50 @@ describe('Langfuse integration', () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty', input: [] })
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
input: [],
})
expect(span).not.toBeNull()
expect(mockStartObservation).toHaveBeenCalledWith('agent-run', expect.objectContaining({
metadata: expect.objectContaining({ provider: 'firstParty', model: 'claude-3' }),
}), { asType: 'agent' })
expect(mockStartObservation).toHaveBeenCalledWith(
'agent-run',
expect.objectContaining({
metadata: expect.objectContaining({
provider: 'firstParty',
model: 'claude-3',
}),
}),
{ asType: 'agent' },
)
})
})
describe('recordLLMObservation', () => {
test('no-ops when rootSpan is null', async () => {
const { recordLLMObservation } = await import('../tracing.js')
recordLLMObservation(null, { model: 'm', provider: 'firstParty', input: [], output: [], usage: { input_tokens: 10, output_tokens: 5 } })
recordLLMObservation(null, {
model: 'm',
provider: 'firstParty',
input: [],
output: [],
usage: { input_tokens: 10, output_tokens: 5 },
})
expect(mockStartObservation).toHaveBeenCalledTimes(0)
})
test('records generation child observation via global startObservation', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordLLMObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordLLMObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockStartObservation.mockClear()
recordLLMObservation(span, {
model: 'claude-3',
@@ -367,23 +409,35 @@ describe('Langfuse integration', () => {
usage: { input_tokens: 10, output_tokens: 5 },
})
// Should call the global startObservation with asType: 'generation' and parentSpanContext
expect(mockStartObservation).toHaveBeenCalledWith('ChatAnthropic', expect.objectContaining({
model: 'claude-3',
}), expect.objectContaining({
asType: 'generation',
parentSpanContext: mockSpanContext,
}))
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({
usageDetails: { input: 10, output: 5 },
}))
expect(mockStartObservation).toHaveBeenCalledWith(
'ChatAnthropic',
expect.objectContaining({
model: 'claude-3',
}),
expect.objectContaining({
asType: 'generation',
parentSpanContext: mockSpanContext,
}),
)
expect(mockRootUpdate).toHaveBeenCalledWith(
expect.objectContaining({
usageDetails: { input: 10, output: 5 },
}),
)
expect(mockRootEnd).toHaveBeenCalled()
})
test('includes cache tokens in usageDetails when provided', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordLLMObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordLLMObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockStartObservation.mockClear()
mockRootUpdate.mockClear()
recordLLMObservation(span, {
@@ -391,23 +445,36 @@ describe('Langfuse integration', () => {
provider: 'firstParty',
input: [],
output: [],
usage: { input_tokens: 10000, output_tokens: 50, cache_creation_input_tokens: 2000, cache_read_input_tokens: 7000 },
})
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({
usageDetails: {
input: 19000, // 10000 + 2000 + 7000
output: 50,
cache_read: 7000,
cache_creation: 2000,
usage: {
input_tokens: 10000,
output_tokens: 50,
cache_creation_input_tokens: 2000,
cache_read_input_tokens: 7000,
},
}))
})
expect(mockRootUpdate).toHaveBeenCalledWith(
expect.objectContaining({
usageDetails: {
input: 19000, // 10000 + 2000 + 7000
output: 50,
cache_read: 7000,
cache_creation: 2000,
},
}),
)
})
test('omits cache fields when not provided', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordLLMObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordLLMObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockRootUpdate.mockClear()
recordLLMObservation(span, {
model: 'claude-3',
@@ -416,24 +483,37 @@ describe('Langfuse integration', () => {
output: [],
usage: { input_tokens: 100, output_tokens: 20 },
})
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({
usageDetails: { input: 100, output: 20 },
}))
expect(mockRootUpdate).toHaveBeenCalledWith(
expect.objectContaining({
usageDetails: { input: 100, output: 20 },
}),
)
})
})
describe('recordToolObservation', () => {
test('no-ops when rootSpan is null', async () => {
const { recordToolObservation } = await import('../tracing.js')
recordToolObservation(null, { toolName: 'BashTool', toolUseId: 'id1', input: {}, output: 'out' })
recordToolObservation(null, {
toolName: 'BashTool',
toolUseId: 'id1',
input: {},
output: 'out',
})
// startObservation should not be called beyond the initial trace creation (none here)
})
test('records tool child observation via global startObservation', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordToolObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordToolObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockStartObservation.mockClear()
mockRootUpdate.mockClear()
mockRootEnd.mockClear()
@@ -444,12 +524,16 @@ describe('Langfuse integration', () => {
output: 'file.ts',
})
// Should call the global startObservation with asType: 'tool' and parentSpanContext
expect(mockStartObservation).toHaveBeenCalledWith('BashTool', expect.objectContaining({
input: expect.any(Object),
}), expect.objectContaining({
asType: 'tool',
parentSpanContext: mockSpanContext,
}))
expect(mockStartObservation).toHaveBeenCalledWith(
'BashTool',
expect.objectContaining({
input: expect.any(Object),
}),
expect.objectContaining({
asType: 'tool',
parentSpanContext: mockSpanContext,
}),
)
expect(mockRootUpdate).toHaveBeenCalled()
expect(mockRootEnd).toHaveBeenCalled()
})
@@ -457,8 +541,14 @@ describe('Langfuse integration', () => {
test('passes startTime to global startObservation', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordToolObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordToolObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockStartObservation.mockClear()
const startTime = new Date('2026-01-01T00:00:00Z')
recordToolObservation(span, {
@@ -468,17 +558,27 @@ describe('Langfuse integration', () => {
output: 'out',
startTime,
})
expect(mockStartObservation).toHaveBeenCalledWith('BashTool', expect.any(Object), expect.objectContaining({
startTime,
parentSpanContext: mockSpanContext,
}))
expect(mockStartObservation).toHaveBeenCalledWith(
'BashTool',
expect.any(Object),
expect.objectContaining({
startTime,
parentSpanContext: mockSpanContext,
}),
)
})
test('sanitizes FileReadTool output', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordToolObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordToolObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockRootUpdate.mockClear()
recordToolObservation(span, {
toolName: 'FileReadTool',
@@ -486,16 +586,24 @@ describe('Langfuse integration', () => {
input: { file_path: '/tmp/file.ts' },
output: 'file content here',
})
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({
output: '[file content redacted, 17 chars]',
}))
expect(mockRootUpdate).toHaveBeenCalledWith(
expect.objectContaining({
output: '[file content redacted, 17 chars]',
}),
)
})
test('sets ERROR level for error observations', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordToolObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordToolObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockRootUpdate.mockClear()
recordToolObservation(span, {
toolName: 'BashTool',
@@ -504,7 +612,9 @@ describe('Langfuse integration', () => {
output: 'error occurred',
isError: true,
})
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({ level: 'ERROR' }))
expect(mockRootUpdate).toHaveBeenCalledWith(
expect.objectContaining({ level: 'ERROR' }),
)
})
})
@@ -519,7 +629,11 @@ describe('Langfuse integration', () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, endTrace } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
endTrace(span)
expect(mockRootEnd).toHaveBeenCalled()
})
@@ -528,7 +642,11 @@ describe('Langfuse integration', () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, endTrace } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
endTrace(span, 'final output')
expect(mockRootUpdate).toHaveBeenCalledWith({ output: 'final output' })
expect(mockRootEnd).toHaveBeenCalled()
@@ -561,14 +679,18 @@ describe('Langfuse integration', () => {
input: [{ role: 'user', content: 'search for X' }],
})
expect(span).not.toBeNull()
expect(mockStartObservation).toHaveBeenCalledWith('agent:Explore', expect.objectContaining({
metadata: expect.objectContaining({
agentType: 'Explore',
agentId: 'agent-1',
provider: 'firstParty',
model: 'claude-3',
expect(mockStartObservation).toHaveBeenCalledWith(
'agent:Explore',
expect.objectContaining({
metadata: expect.objectContaining({
agentType: 'Explore',
agentId: 'agent-1',
provider: 'firstParty',
model: 'claude-3',
}),
}),
}), { asType: 'agent' })
{ asType: 'agent' },
)
// Verify session.id attribute is set
expect(mockSetAttribute).toHaveBeenCalledWith('session.id', 's1')
})
@@ -576,7 +698,9 @@ describe('Langfuse integration', () => {
test('returns null on SDK error', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
mockStartObservation.mockImplementationOnce(() => { throw new Error('SDK error') })
mockStartObservation.mockImplementationOnce(() => {
throw new Error('SDK error')
})
const { createSubagentTrace } = await import('../tracing.js')
const span = createSubagentTrace({
sessionId: 's1',
@@ -601,12 +725,16 @@ describe('Langfuse integration', () => {
querySource: 'user',
})
expect(span).not.toBeNull()
expect(mockStartObservation).toHaveBeenCalledWith('agent-run:user', expect.objectContaining({
metadata: expect.objectContaining({
agentType: 'main',
querySource: 'user',
expect(mockStartObservation).toHaveBeenCalledWith(
'agent-run:user',
expect.objectContaining({
metadata: expect.objectContaining({
agentType: 'main',
querySource: 'user',
}),
}),
}), { asType: 'agent' })
{ asType: 'agent' },
)
})
test('omits querySource when not provided', async () => {
@@ -614,7 +742,11 @@ describe('Langfuse integration', () => {
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
mockStartObservation.mockClear()
const { createTrace } = await import('../tracing.js')
createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
const calls = mockStartObservation.mock.calls as unknown[][]
const secondArg = calls[0]?.[1] as Record<string, unknown> | undefined
const metadata = (secondArg?.metadata ?? {}) as Record<string, unknown>
@@ -635,7 +767,10 @@ describe('Langfuse integration', () => {
username: 'user@example.com',
})
expect(span).not.toBeNull()
expect(mockSetAttribute).toHaveBeenCalledWith('user.id', 'user@example.com')
expect(mockSetAttribute).toHaveBeenCalledWith(
'user.id',
'user@example.com',
)
})
test('falls back to LANGFUSE_USER_ID env when username not provided', async () => {
@@ -650,7 +785,10 @@ describe('Langfuse integration', () => {
provider: 'firstParty',
})
expect(span).not.toBeNull()
expect(mockSetAttribute).toHaveBeenCalledWith('user.id', 'env-user@test.com')
expect(mockSetAttribute).toHaveBeenCalledWith(
'user.id',
'env-user@test.com',
)
delete process.env.LANGFUSE_USER_ID
})
@@ -660,7 +798,11 @@ describe('Langfuse integration', () => {
delete process.env.LANGFUSE_USER_ID
mockSetAttribute.mockClear()
const { createTrace } = await import('../tracing.js')
createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
// Falls back to getCoreUserData().deviceId (mocked as 'test-device-id')
expect(mockSetAttribute).toHaveBeenCalledWith('user.id', 'test-device-id')
})
@@ -714,7 +856,10 @@ describe('Langfuse integration', () => {
// Both should have set session.id attribute
const sessionAttributeCalls = mockSetAttribute.mock.calls.filter(
(call: unknown[]) => Array.isArray(call) && call[0] === 'session.id' && call[1] === 'shared-session',
(call: unknown[]) =>
Array.isArray(call) &&
call[0] === 'session.id' &&
call[1] === 'shared-session',
)
expect(sessionAttributeCalls.length).toBeGreaterThanOrEqual(2)
})
@@ -739,8 +884,8 @@ describe('Langfuse integration', () => {
expect(subTrace).not.toBeNull()
// Simulate query.ts logic: if langfuseTrace already set, don't create new one
const ownsTrace = false // Would be: !params.toolUseContext.langfuseTrace
const langfuseTrace = subTrace // Would be: params.toolUseContext.langfuseTrace ?? createTrace(...)
const ownsTrace = false // Would be: !params.toolUseContext.langfuseTrace
const langfuseTrace = subTrace // Would be: params.toolUseContext.langfuseTrace ?? createTrace(...)
expect(ownsTrace).toBe(false)
expect(langfuseTrace).toBe(subTrace)
@@ -761,7 +906,9 @@ describe('Langfuse integration', () => {
},
},
]
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
const result = convertToolsToLangfuse(tools) as Array<
Record<string, unknown>
>
expect(result).toHaveLength(1)
expect(result[0]).toEqual({
type: 'function',
@@ -780,22 +927,44 @@ describe('Langfuse integration', () => {
test('converts multiple tools', async () => {
const { convertToolsToLangfuse } = await import('../convert.js')
const tools = [
{ name: 'ReadTool', description: 'Read a file', input_schema: { type: 'object' } },
{ name: 'WriteTool', description: 'Write a file', input_schema: { type: 'object' } },
{
name: 'ReadTool',
description: 'Read a file',
input_schema: { type: 'object' },
},
{
name: 'WriteTool',
description: 'Write a file',
input_schema: { type: 'object' },
},
]
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
const result = convertToolsToLangfuse(tools) as Array<
Record<string, unknown>
>
expect(result).toHaveLength(2)
expect((result[0]!.function as Record<string, unknown>).name).toBe('ReadTool')
expect((result[1]!.function as Record<string, unknown>).name).toBe('WriteTool')
expect((result[0]!.function as Record<string, unknown>).name).toBe(
'ReadTool',
)
expect((result[1]!.function as Record<string, unknown>).name).toBe(
'WriteTool',
)
})
test('falls back to parameters when input_schema is missing', async () => {
const { convertToolsToLangfuse } = await import('../convert.js')
const tools = [
{ name: 'Tool1', description: 'desc', parameters: { type: 'object', properties: { a: { type: 'string' } } } },
{
name: 'Tool1',
description: 'desc',
parameters: { type: 'object', properties: { a: { type: 'string' } } },
},
]
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
expect((result[0]!.function as Record<string, unknown>).parameters).toEqual({
const result = convertToolsToLangfuse(tools) as Array<
Record<string, unknown>
>
expect(
(result[0]!.function as Record<string, unknown>).parameters,
).toEqual({
type: 'object',
properties: { a: { type: 'string' } },
})
@@ -804,8 +973,12 @@ describe('Langfuse integration', () => {
test('uses empty object when neither input_schema nor parameters exist', async () => {
const { convertToolsToLangfuse } = await import('../convert.js')
const tools = [{ name: 'Tool1', description: 'desc' }]
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
expect((result[0]!.function as Record<string, unknown>).parameters).toEqual({})
const result = convertToolsToLangfuse(tools) as Array<
Record<string, unknown>
>
expect(
(result[0]!.function as Record<string, unknown>).parameters,
).toEqual({})
})
test('returns empty array for empty input', async () => {
@@ -818,11 +991,22 @@ describe('Langfuse integration', () => {
test('wraps input into { messages, tools } when tools provided', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordLLMObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordLLMObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockStartObservation.mockClear()
const messages = [{ role: 'user', content: 'hello' }]
const tools = [{ type: 'function', function: { name: 'Bash', description: 'Run', parameters: {} } }]
const tools = [
{
type: 'function',
function: { name: 'Bash', description: 'Run', parameters: {} },
},
]
recordLLMObservation(span, {
model: 'claude-3',
provider: 'firstParty',
@@ -831,18 +1015,28 @@ describe('Langfuse integration', () => {
usage: { input_tokens: 10, output_tokens: 5 },
tools,
})
expect(mockStartObservation).toHaveBeenCalledWith('ChatAnthropic', expect.objectContaining({
input: { messages, tools },
}), expect.objectContaining({
asType: 'generation',
}))
expect(mockStartObservation).toHaveBeenCalledWith(
'ChatAnthropic',
expect.objectContaining({
input: { messages, tools },
}),
expect.objectContaining({
asType: 'generation',
}),
)
})
test('keeps input as-is when tools not provided', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
const { createTrace, recordLLMObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const { createTrace, recordLLMObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
mockStartObservation.mockClear()
const messages = [{ role: 'user', content: 'hello' }]
recordLLMObservation(span, {
@@ -852,9 +1046,13 @@ describe('Langfuse integration', () => {
output: [],
usage: { input_tokens: 10, output_tokens: 5 },
})
expect(mockStartObservation).toHaveBeenCalledWith('ChatAnthropic', expect.objectContaining({
input: messages,
}), expect.any(Object))
expect(mockStartObservation).toHaveBeenCalledWith(
'ChatAnthropic',
expect.objectContaining({
input: messages,
}),
expect.any(Object),
)
})
})
@@ -862,27 +1060,45 @@ describe('Langfuse integration', () => {
test('createTrace returns null on SDK error', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
mockStartObservation.mockImplementationOnce(() => { throw new Error('SDK error') })
mockStartObservation.mockImplementationOnce(() => {
throw new Error('SDK error')
})
const { createTrace } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
})
expect(span).toBeNull()
})
test('recordLLMObservation silently fails on SDK error', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
mockStartObservation.mockImplementationOnce(() => { throw new Error('SDK error') })
const { createTrace, recordLLMObservation } = await import('../tracing.js')
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
// The second call to startObservation (for the generation) will throw
mockStartObservation.mockImplementationOnce(() => { throw new Error('SDK error') })
expect(() => recordLLMObservation(span, {
model: 'm',
mockStartObservation.mockImplementationOnce(() => {
throw new Error('SDK error')
})
const { createTrace, recordLLMObservation } = await import(
'../tracing.js'
)
const span = createTrace({
sessionId: 's1',
model: 'claude-3',
provider: 'firstParty',
input: [],
output: [],
usage: { input_tokens: 1, output_tokens: 1 },
})).not.toThrow()
})
// The second call to startObservation (for the generation) will throw
mockStartObservation.mockImplementationOnce(() => {
throw new Error('SDK error')
})
expect(() =>
recordLLMObservation(span, {
model: 'm',
provider: 'firstParty',
input: [],
output: [],
usage: { input_tokens: 1, output_tokens: 1 },
}),
).not.toThrow()
})
})
})

View File

@@ -37,7 +37,11 @@ export function initLangfuse(): boolean {
mask: maskFn,
environment: process.env.LANGFUSE_TRACING_ENVIRONMENT ?? 'development',
release: MACRO.VERSION,
exportMode: (process.env.LANGFUSE_EXPORT_MODE as 'batched' | 'immediate' | undefined) ?? 'batched',
exportMode:
(process.env.LANGFUSE_EXPORT_MODE as
| 'batched'
| 'immediate'
| undefined) ?? 'batched',
timeout: parseInt(process.env.LANGFUSE_TIMEOUT ?? '5', 10),
})

View File

@@ -74,7 +74,8 @@ function mergeToolCalls(
): LangfuseToolCall[] {
const merged = new Map<string, LangfuseToolCall>()
for (const toolCall of groups.flat()) {
const key = toolCall.id || `${toolCall.function.name}:${toolCall.function.arguments}`
const key =
toolCall.id || `${toolCall.function.name}:${toolCall.function.arguments}`
if (!merged.has(key)) merged.set(key, toolCall)
}
return [...merged.values()]
@@ -87,40 +88,65 @@ type LangfuseInputMessage =
| ChatCompletionMessageParam
/** Normalize a content block into a LangfuseContentPart (non-tool_use, non-tool_result) */
function toContentPart(block: Record<string, unknown>): LangfuseContentPart | null {
function toContentPart(
block: Record<string, unknown>,
): LangfuseContentPart | null {
const type = block.type as string | undefined
if (type === 'text') {
return { type: 'text', text: String(block.text ?? '') }
}
if (type === 'thinking' || type === 'redacted_thinking') {
return { type: 'thinking', thinking: String(block.thinking ?? '[redacted]') }
return {
type: 'thinking',
thinking: String(block.thinking ?? '[redacted]'),
}
}
if (type === 'image') {
return { type: 'text', text: '[image]' }
}
if (type === 'document') {
const name = (block.source as Record<string, unknown> | undefined)?.filename
?? (block.title as string | undefined)
?? 'document'
const name =
(block.source as Record<string, unknown> | undefined)?.filename ??
(block.title as string | undefined) ??
'document'
return { type: 'text', text: `[document: ${name}]` }
}
if (type === 'server_tool_use' || type === 'web_search_tool_result' || type === 'tool_search_tool_result') {
return { type, id: String(block.id ?? ''), name: String(block.name ?? type) }
if (
type === 'server_tool_use' ||
type === 'web_search_tool_result' ||
type === 'tool_search_tool_result'
) {
return {
type,
id: String(block.id ?? ''),
name: String(block.name ?? type),
}
}
// unknown block: keep type + scalar fields only
const safe: Record<string, unknown> = { type: type ?? 'unknown' }
for (const [k, v] of Object.entries(block)) {
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') safe[k] = v
if (
typeof v === 'string' ||
typeof v === 'number' ||
typeof v === 'boolean'
)
safe[k] = v
}
return safe as LangfuseContentPart
}
/** Extract tool_use blocks from content into OpenAI-style tool_calls */
function extractToolCalls(content: unknown[]): { tool_calls: LangfuseToolCall[]; rest: unknown[] } {
function extractToolCalls(content: unknown[]): {
tool_calls: LangfuseToolCall[]
rest: unknown[]
} {
const toolCalls: LangfuseToolCall[] = []
const rest: unknown[] = []
for (const block of content) {
if (!block || typeof block !== 'object') { rest.push(block); continue }
if (!block || typeof block !== 'object') {
rest.push(block)
continue
}
const b = block as Record<string, unknown>
if (b.type === 'tool_use') {
toolCalls.push({
@@ -128,7 +154,10 @@ function extractToolCalls(content: unknown[]): { tool_calls: LangfuseToolCall[];
type: 'function',
function: {
name: String(b.name ?? ''),
arguments: typeof b.input === 'string' ? b.input : JSON.stringify(b.input ?? {}),
arguments:
typeof b.input === 'string'
? b.input
: JSON.stringify(b.input ?? {}),
},
})
} else {
@@ -139,11 +168,17 @@ function extractToolCalls(content: unknown[]): { tool_calls: LangfuseToolCall[];
}
/** Extract tool_result blocks into separate { role: 'tool' } messages */
function extractToolResults(content: unknown[]): { toolMessages: LangfuseChatMessage[]; rest: unknown[] } {
function extractToolResults(content: unknown[]): {
toolMessages: LangfuseChatMessage[]
rest: unknown[]
} {
const toolMessages: LangfuseChatMessage[] = []
const rest: unknown[] = []
for (const block of content) {
if (!block || typeof block !== 'object') { rest.push(block); continue }
if (!block || typeof block !== 'object') {
rest.push(block)
continue
}
const b = block as Record<string, unknown>
if (b.type === 'tool_result') {
const resultContent = Array.isArray(b.content)
@@ -169,7 +204,9 @@ function extractToolResults(content: unknown[]): { toolMessages: LangfuseChatMes
}
/** Collapse content parts: join all-text arrays into a single string */
function collapseContent(parts: LangfuseContentPart[]): string | LangfuseContentPart[] {
function collapseContent(
parts: LangfuseContentPart[],
): string | LangfuseContentPart[] {
if (parts.length === 0) return ''
if (parts.every(p => p.type === 'text')) {
return parts.map(p => (p as { type: 'text'; text: string }).text).join('\n')
@@ -177,7 +214,9 @@ function collapseContent(parts: LangfuseContentPart[]): string | LangfuseContent
return parts
}
function toRoleFromWrappedMessage(msg: Record<string, unknown>): 'user' | 'assistant' | 'system' {
function toRoleFromWrappedMessage(
msg: Record<string, unknown>,
): 'user' | 'assistant' | 'system' {
if (msg.type === 'assistant') return 'assistant'
if (msg.type === 'system') return 'system'
return 'user'
@@ -199,8 +238,11 @@ export function convertMessagesToLangfuse(
const wrappedMessage = msg.message
const isWrappedMessage = isRecord(wrappedMessage)
const inner = isWrappedMessage ? wrappedMessage : msg
const role =
isLangfuseRole(inner.role) ? inner.role : isWrappedMessage ? toRoleFromWrappedMessage(msg) : 'user'
const role = isLangfuseRole(inner.role)
? inner.role
: isWrappedMessage
? toRoleFromWrappedMessage(msg)
: 'user'
const rawContent = inner.content
if (typeof rawContent === 'string' || !Array.isArray(rawContent)) {
const toolCalls = getToolCalls(inner.tool_calls)
@@ -224,7 +266,10 @@ export function convertMessagesToLangfuse(
getContentToolCalls(rest),
)
const parts = rest
.filter((b): b is Record<string, unknown> => b != null && typeof b === 'object')
.filter(
(b): b is Record<string, unknown> =>
b != null && typeof b === 'object',
)
.map(b => toContentPart(b))
.filter((p): p is LangfuseContentPart => p !== null)
result.push({
@@ -236,7 +281,10 @@ export function convertMessagesToLangfuse(
// User messages: extract tool_result → separate tool messages
const { toolMessages, rest } = extractToolResults(rawContent)
const parts = rest
.filter((b): b is Record<string, unknown> => b != null && typeof b === 'object')
.filter(
(b): b is Record<string, unknown> =>
b != null && typeof b === 'object',
)
.map(b => toContentPart(b))
.filter((p): p is LangfuseContentPart => p !== null)
if (parts.length > 0 || toolMessages.length === 0) {
@@ -287,7 +335,9 @@ export function convertOutputToLangfuse(
}
const { tool_calls, rest } = extractToolCalls(rawContent)
const parts = rest
.filter((b): b is Record<string, unknown> => b != null && typeof b === 'object')
.filter(
(b): b is Record<string, unknown> => b != null && typeof b === 'object',
)
.map(b => toContentPart(b))
.filter((p): p is LangfuseContentPart => p !== null)
return {

View File

@@ -1,4 +1,22 @@
export { initLangfuse, shutdownLangfuse, isLangfuseEnabled, getLangfuseProcessor } from './client.js'
export { createTrace, createSubagentTrace, createChildSpan, recordLLMObservation, recordToolObservation, endTrace, createToolBatchSpan, endToolBatchSpan } from './tracing.js'
export {
initLangfuse,
shutdownLangfuse,
isLangfuseEnabled,
getLangfuseProcessor,
} from './client.js'
export {
createTrace,
createSubagentTrace,
createChildSpan,
recordLLMObservation,
recordToolObservation,
endTrace,
createToolBatchSpan,
endToolBatchSpan,
} from './tracing.js'
export type { LangfuseSpan } from './tracing.js'
export { sanitizeToolInput, sanitizeToolOutput, sanitizeGlobal } from './sanitize.js'
export {
sanitizeToolInput,
sanitizeToolOutput,
sanitizeGlobal,
} from './sanitize.js'

View File

@@ -1,7 +1,11 @@
import { homedir } from 'os'
const MAX_OUTPUT_LENGTH = 500
const REDACTED_FILE_TOOLS = new Set(['FileReadTool', 'FileWriteTool', 'FileEditTool'])
const REDACTED_FILE_TOOLS = new Set([
'FileReadTool',
'FileWriteTool',
'FileEditTool',
])
const REDACTED_SHELL_TOOLS = new Set(['BashTool', 'PowerShellTool'])
const SENSITIVE_OUTPUT_TOOLS = new Set(['ConfigTool', 'MCPTool'])
@@ -27,7 +31,8 @@ function homePathPatterns(): string[] {
const HOME_DIR_PATTERN = new RegExp(`(?:${homePathPatterns().join('|')})`, 'g')
const SENSITIVE_KEY_PATTERN = /(?:api_?key|token|secret|password|credential|auth_header)/i
const SENSITIVE_KEY_PATTERN =
/(?:api_?key|token|secret|password|credential|auth_header)/i
export function sanitizeGlobal(data: unknown): unknown {
if (typeof data === 'string') {

View File

@@ -1,5 +1,9 @@
import { startObservation, LangfuseOtelSpanAttributes } from '@langfuse/tracing'
import type { LangfuseSpan, LangfuseGeneration, LangfuseAgent } from '@langfuse/tracing'
import type {
LangfuseSpan,
LangfuseGeneration,
LangfuseAgent,
} from '@langfuse/tracing'
import { isLangfuseEnabled } from './client.js'
import { sanitizeToolInput, sanitizeToolOutput } from './sanitize.js'
import { logForDebugging } from 'src/utils/debug.js'
@@ -12,7 +16,12 @@ type RootTrace = LangfuseAgent & { _sessionId?: string; _userId?: string }
/** Resolve the user ID for Langfuse traces: explicit param > env var > email > deviceId */
function resolveLangfuseUserId(username?: string): string | undefined {
return username ?? process.env.LANGFUSE_USER_ID ?? getCoreUserData().email ?? getCoreUserData().deviceId
return (
username ??
process.env.LANGFUSE_USER_ID ??
getCoreUserData().email ??
getCoreUserData().deviceId
)
}
export function createTrace(params: {
@@ -26,21 +35,33 @@ export function createTrace(params: {
}): LangfuseSpan | null {
if (!isLangfuseEnabled()) return null
try {
const traceName = params.name ?? (params.querySource ? `agent-run:${params.querySource}` : 'agent-run')
const rootSpan = startObservation(traceName, {
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
agentType: 'main',
...(params.querySource && { querySource: params.querySource }),
const traceName =
params.name ??
(params.querySource ? `agent-run:${params.querySource}` : 'agent-run')
const rootSpan = startObservation(
traceName,
{
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
agentType: 'main',
...(params.querySource && { querySource: params.querySource }),
},
},
}, { asType: 'agent' }) as RootTrace
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
{ asType: 'agent' },
) as RootTrace
rootSpan.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_SESSION_ID,
params.sessionId,
)
rootSpan._sessionId = params.sessionId
const userId = resolveLangfuseUserId(params.username)
if (userId) {
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
rootSpan.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_USER_ID,
userId,
)
rootSpan._userId = userId
}
logForDebugging(`[langfuse] Trace created: ${rootSpan.id}`)
@@ -92,7 +113,8 @@ export function recordLLMObservation(
): void {
if (!rootSpan || !isLangfuseEnabled()) return
try {
const genName = PROVIDER_GENERATION_NAMES[params.provider] ?? `Chat${params.provider}`
const genName =
PROVIDER_GENERATION_NAMES[params.provider] ?? `Chat${params.provider}`
// Use the global startObservation directly instead of rootSpan.startObservation().
// The instance method only forwards asType to the global function and drops startTime,
@@ -109,7 +131,9 @@ export function recordLLMObservation(
model: params.model,
...(params.thinking && { thinking: params.thinking }),
},
...(params.completionStartTime && { completionStartTime: params.completionStartTime }),
...(params.completionStartTime && {
completionStartTime: params.completionStartTime,
}),
},
{
asType: 'generation',
@@ -121,11 +145,17 @@ export function recordLLMObservation(
// Propagate session ID and user ID to generation span so Langfuse links it correctly
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
if (sessionId) {
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
gen.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_SESSION_ID,
sessionId,
)
}
const userId = (rootSpan as unknown as RootTrace)._userId
if (userId) {
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
gen.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_USER_ID,
userId,
)
}
// Anthropic splits input into uncached + cache_read + cache_creation.
@@ -145,7 +175,9 @@ export function recordLLMObservation(
gen.end(params.endTime)
logForDebugging(`[langfuse] LLM observation recorded: ${gen.id}`)
} catch (e) {
logForDebugging(`[langfuse] recordLLMObservation failed: ${e}`, { level: 'error' })
logForDebugging(`[langfuse] recordLLMObservation failed: ${e}`, {
level: 'error',
})
}
}
@@ -186,11 +218,17 @@ export function recordToolObservation(
// Propagate session ID and user ID to tool span so Langfuse links it correctly
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
if (sessionId) {
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
toolObs.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_SESSION_ID,
sessionId,
)
}
const userId = (rootSpan as unknown as RootTrace)._userId
if (userId) {
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
toolObs.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_USER_ID,
userId,
)
}
toolObs.update({
@@ -199,9 +237,13 @@ export function recordToolObservation(
})
toolObs.end()
logForDebugging(`[langfuse] Tool observation recorded: ${params.toolName} (${toolObs.id})`)
logForDebugging(
`[langfuse] Tool observation recorded: ${params.toolName} (${toolObs.id})`,
)
} catch (e) {
logForDebugging(`[langfuse] recordToolObservation failed: ${e}`, { level: 'error' })
logForDebugging(`[langfuse] recordToolObservation failed: ${e}`, {
level: 'error',
})
}
}
@@ -233,17 +275,27 @@ export function createToolBatchSpan(
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
if (sessionId) {
batchSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
batchSpan.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_SESSION_ID,
sessionId,
)
}
const userId = (rootSpan as unknown as RootTrace)._userId
if (userId) {
batchSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
batchSpan.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_USER_ID,
userId,
)
}
logForDebugging(`[langfuse] Tool batch span created: ${batchSpan.id} (tools=${params.toolNames.join(',')})`)
logForDebugging(
`[langfuse] Tool batch span created: ${batchSpan.id} (tools=${params.toolNames.join(',')})`,
)
return batchSpan
} catch (e) {
logForDebugging(`[langfuse] createToolBatchSpan failed: ${e}`, { level: 'error' })
logForDebugging(`[langfuse] createToolBatchSpan failed: ${e}`, {
level: 'error',
})
return null
}
}
@@ -254,7 +306,9 @@ export function endToolBatchSpan(batchSpan: LangfuseSpan | null): void {
batchSpan.end()
logForDebugging(`[langfuse] Tool batch span ended: ${batchSpan.id}`)
} catch (e) {
logForDebugging(`[langfuse] endToolBatchSpan failed: ${e}`, { level: 'error' })
logForDebugging(`[langfuse] endToolBatchSpan failed: ${e}`, {
level: 'error',
})
}
}
@@ -269,26 +323,40 @@ export function createSubagentTrace(params: {
}): LangfuseSpan | null {
if (!isLangfuseEnabled()) return null
try {
const rootSpan = startObservation(`agent:${params.agentType}`, {
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
agentType: params.agentType,
agentId: params.agentId,
const rootSpan = startObservation(
`agent:${params.agentType}`,
{
input: params.input,
metadata: {
provider: params.provider,
model: params.model,
agentType: params.agentType,
agentId: params.agentId,
},
},
}, { asType: 'agent' }) as RootTrace
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
{ asType: 'agent' },
) as RootTrace
rootSpan.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_SESSION_ID,
params.sessionId,
)
rootSpan._sessionId = params.sessionId
const userId = resolveLangfuseUserId(params.username)
if (userId) {
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
rootSpan.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_USER_ID,
userId,
)
rootSpan._userId = userId
}
logForDebugging(`[langfuse] Sub-agent trace created: ${rootSpan.id} (type=${params.agentType})`)
logForDebugging(
`[langfuse] Sub-agent trace created: ${rootSpan.id} (type=${params.agentType})`,
)
return rootSpan as unknown as LangfuseSpan
} catch (e) {
logForDebugging(`[langfuse] createSubagentTrace failed: ${e}`, { level: 'error' })
logForDebugging(`[langfuse] createSubagentTrace failed: ${e}`, {
level: 'error',
})
return null
}
}
@@ -331,18 +399,28 @@ export function createChildSpan(
const parent = parentSpan as unknown as RootTrace
const sessionId = parent._sessionId ?? params.sessionId
if (sessionId) {
span.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
span.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_SESSION_ID,
sessionId,
)
;(span as unknown as RootTrace)._sessionId = sessionId
}
const userId = parent._userId ?? resolveLangfuseUserId(params.username)
if (userId) {
span.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
span.otelSpan.setAttribute(
LangfuseOtelSpanAttributes.TRACE_USER_ID,
userId,
)
;(span as unknown as RootTrace)._userId = userId
}
logForDebugging(`[langfuse] Child span created: ${span.id} (parent=${parentSpan.id})`)
logForDebugging(
`[langfuse] Child span created: ${span.id} (parent=${parentSpan.id})`,
)
return span
} catch (e) {
logForDebugging(`[langfuse] createChildSpan failed: ${e}`, { level: 'error' })
logForDebugging(`[langfuse] createChildSpan failed: ${e}`, {
level: 'error',
})
return null
}
}
@@ -360,7 +438,9 @@ export function endTrace(
else if (status === 'error') updatePayload.level = 'ERROR'
if (Object.keys(updatePayload).length > 0) rootSpan.update(updatePayload)
rootSpan.end()
logForDebugging(`[langfuse] Trace ended: ${rootSpan.id}${status ? ` (${status})` : ''}`)
logForDebugging(
`[langfuse] Trace ended: ${rootSpan.id}${status ? ` (${status})` : ''}`,
)
} catch (e) {
logForDebugging(`[langfuse] endTrace failed: ${e}`, { level: 'error' })
}