mirror of
https://github.com/hanzoai/insights-js.git
synced 2026-10-30 18:34:19 +00:00
We own the ingestion server (hanzoai/insights: Django + rust/capture + nodejs plugin) AND this SDK, so there is no external wire contract to preserve. The server is already 100% posthog-free; this SDK was the last laggard. - $last_posthog_reset -> $last_insights_reset. This FIXES A LIVE MISMATCH: the server taxonomy (insights/taxonomy/taxonomy.py, insights/insightsql/ai.py) already expected the insights form while the SDK emitted the posthog form. - __PosthogExtensions__ -> __InsightsExtensions__; all PostHog/posthog/POSTHOG identifiers -> Insights/insights/INSIGHTS (src, tests, playground, playwright, scripts, rrweb subpackages, package.json keywords). - Hosts: DEFAULT_PLUGIN_HOST + DEFAULT_NUXT_HOST + the core host default pointed at PostHog's cloud (us.i.posthog.com) — a real bug. All -> insights.hanzo.ai. - Removed the us-assets/eu-assets CDN split in getRemoteConfig (PostHog-specific; Hanzo serves remote config from the same host) — it had degenerated to a no-op. - Fixed generate-docs writing to a stray references/@hanzo/ subdir. - Renamed the 3 posthog-named test files; dropped 58MB of stale reference snapshots named for upstream posthog versions we never shipped (they are generated output — references/ is now gitignored). Verified: typecheck errors 42 (HEAD) -> 18 (all pre-existing test-mock looseness); types package 0; every package.json valid with @hanzo/* names. Guard: grep -rioE 'posthog' packages ... | wc -l => 0
160 lines
5.3 KiB
TypeScript
160 lines
5.3 KiB
TypeScript
import type { SpanProcessor, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'
|
|
import type { Context } from '@opentelemetry/api'
|
|
import { InsightsSpanProcessor } from '../src/otel'
|
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
|
|
jest.mock('@opentelemetry/exporter-trace-otlp-http', () => ({
|
|
OTLPTraceExporter: jest.fn(),
|
|
}))
|
|
|
|
jest.mock('@opentelemetry/sdk-trace-base', () => ({
|
|
BatchSpanProcessor: jest.fn().mockImplementation(() => ({
|
|
onStart: jest.fn(),
|
|
onEnd: jest.fn(),
|
|
shutdown: jest.fn().mockResolvedValue(undefined),
|
|
forceFlush: jest.fn().mockResolvedValue(undefined),
|
|
})),
|
|
}))
|
|
|
|
function mockProcessor(): SpanProcessor & { onStart: jest.Mock; onEnd: jest.Mock } {
|
|
return {
|
|
onStart: jest.fn(),
|
|
onEnd: jest.fn(),
|
|
shutdown: jest.fn().mockResolvedValue(undefined),
|
|
forceFlush: jest.fn().mockResolvedValue(undefined),
|
|
}
|
|
}
|
|
|
|
function makeSpan(name: string, attributes: Record<string, unknown> = {}): ReadableSpan {
|
|
return { name, attributes } as unknown as ReadableSpan
|
|
}
|
|
|
|
describe('InsightsSpanProcessor', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
name: 'default host',
|
|
apiKey: 'phc_test123',
|
|
host: undefined,
|
|
expectedUrl: 'https://insights.hanzo.ai/i/v0/ai/otel',
|
|
expectedApiKey: 'phc_test123',
|
|
},
|
|
{
|
|
name: 'custom host',
|
|
apiKey: 'phc_test456',
|
|
host: 'https://insights.hanzo.ai',
|
|
expectedUrl: 'https://insights.hanzo.ai/i/v0/ai/otel',
|
|
expectedApiKey: 'phc_test456',
|
|
},
|
|
{
|
|
name: 'trailing slash',
|
|
apiKey: 'phc_test789',
|
|
host: 'https://custom.insights.example.com/',
|
|
expectedUrl: 'https://custom.insights.example.com/i/v0/ai/otel',
|
|
expectedApiKey: 'phc_test789',
|
|
},
|
|
{
|
|
name: 'trimmed whitespace-sensitive values',
|
|
apiKey: ' phc_test999\t ',
|
|
host: ' https://custom.insights.example.com/\n',
|
|
expectedUrl: 'https://custom.insights.example.com/i/v0/ai/otel',
|
|
expectedApiKey: 'phc_test999',
|
|
},
|
|
])('configures the OTLP exporter correctly with $name', ({ apiKey, host, expectedUrl, expectedApiKey }) => {
|
|
new InsightsSpanProcessor({ apiKey, host })
|
|
|
|
expect(OTLPTraceExporter).toHaveBeenCalledWith({
|
|
url: expectedUrl,
|
|
headers: { Authorization: `Bearer ${expectedApiKey}` },
|
|
})
|
|
expect(BatchSpanProcessor).toHaveBeenCalledWith(expect.any(Object))
|
|
})
|
|
|
|
it.each([
|
|
['missing', {}],
|
|
['empty', { apiKey: '' }],
|
|
['blank', { apiKey: ' \n\t ' }],
|
|
])('disables and no-ops when apiKey is %s', async (_case, options) => {
|
|
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
const processor = new InsightsSpanProcessor(options as any)
|
|
|
|
processor.onStart({} as Span, {} as Context)
|
|
processor.onEnd(makeSpan('gen_ai.chat'))
|
|
await expect(processor.shutdown()).resolves.toBeUndefined()
|
|
await expect(processor.forceFlush()).resolves.toBeUndefined()
|
|
|
|
expect(OTLPTraceExporter).not.toHaveBeenCalled()
|
|
expect(BatchSpanProcessor).not.toHaveBeenCalled()
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'[InsightsSpanProcessor] apiKey is missing or blank; the processor will be disabled.'
|
|
)
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('delegates onStart to the inner processor', () => {
|
|
const inner = mockProcessor()
|
|
const processor = new InsightsSpanProcessor({ apiKey: 'phc_test', _spanProcessor: inner })
|
|
|
|
const span = {} as Span
|
|
const ctx = {} as Context
|
|
processor.onStart(span, ctx)
|
|
|
|
expect(inner.onStart).toHaveBeenCalledWith(span, ctx)
|
|
})
|
|
|
|
it('delegates shutdown', async () => {
|
|
const inner = mockProcessor()
|
|
const processor = new InsightsSpanProcessor({ apiKey: 'phc_test', _spanProcessor: inner })
|
|
|
|
await processor.shutdown()
|
|
expect(inner.shutdown).toHaveBeenCalled()
|
|
})
|
|
|
|
it('delegates forceFlush', async () => {
|
|
const inner = mockProcessor()
|
|
const processor = new InsightsSpanProcessor({ apiKey: 'phc_test', _spanProcessor: inner })
|
|
|
|
await processor.forceFlush()
|
|
expect(inner.forceFlush).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('InsightsSpanProcessor AI span filtering', () => {
|
|
it('forwards spans with AI name prefixes', () => {
|
|
const inner = mockProcessor()
|
|
const processor = new InsightsSpanProcessor({ apiKey: 'phc_test', _spanProcessor: inner })
|
|
|
|
processor.onEnd(makeSpan('gen_ai.chat'))
|
|
processor.onEnd(makeSpan('llm.completion'))
|
|
processor.onEnd(makeSpan('ai.invoke'))
|
|
processor.onEnd(makeSpan('traceloop.workflow'))
|
|
|
|
expect(inner.onEnd).toHaveBeenCalledTimes(4)
|
|
})
|
|
|
|
it('drops non-AI spans', () => {
|
|
const inner = mockProcessor()
|
|
const processor = new InsightsSpanProcessor({ apiKey: 'phc_test', _spanProcessor: inner })
|
|
|
|
processor.onEnd(makeSpan('http.request'))
|
|
processor.onEnd(makeSpan('db.query'))
|
|
processor.onEnd(makeSpan('custom.operation'))
|
|
|
|
expect(inner.onEnd).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('detects AI spans by attribute keys', () => {
|
|
const inner = mockProcessor()
|
|
const processor = new InsightsSpanProcessor({ apiKey: 'phc_test', _spanProcessor: inner })
|
|
|
|
processor.onEnd(makeSpan('some.operation', { 'gen_ai.model': 'gpt-4' }))
|
|
processor.onEnd(makeSpan('other.operation', { 'http.method': 'GET' }))
|
|
|
|
expect(inner.onEnd).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|