Files
zeekay ec6f6b50c1 chore: kill ALL posthog — zero refs, zero filenames (1452 -> 0)
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
2026-07-16 09:09:18 -07:00

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)
})
})