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

1058 lines
33 KiB
TypeScript

import { Insights } from '@hanzo/insights-node'
import InsightsGemini from '../src/gemini'
import { version } from '../package.json'
let mockGeminiResponse: any = {}
let mockGeminiStreamResponse: any = {}
jest.mock('@hanzo/insights-node', () => {
return {
Insights: jest.fn().mockImplementation(() => {
return {
capture: jest.fn(),
captureImmediate: jest.fn(),
privacyMode: false,
}
}),
}
})
jest.mock('@google/genai', () => {
class MockGoogleGenAI {
models: any
constructor() {
this.models = {
generateContent: jest.fn(),
generateContentStream: jest.fn(),
embedContent: jest.fn(),
}
}
}
return {
GoogleGenAI: MockGoogleGenAI,
}
})
// Helper function to mock generateContentStream with provided chunks
const mockGenerateContentStream = (chunks: any[]) => {
return jest.fn().mockImplementation(() => {
return (async function* () {
for (const chunk of chunks) {
yield chunk
}
})()
})
}
describe('InsightsGemini - Jest test suite', () => {
let mockInsightsClient: Insights
let client: InsightsGemini
beforeEach(() => {
jest.clearAllMocks()
// Reset the default mocks
mockInsightsClient = new (Insights as any)()
client = new InsightsGemini({
apiKey: 'test-api-key',
insights: mockInsightsClient as any,
})
// Some default generate content mock
mockGeminiResponse = {
text: 'Hello from Gemini!',
candidates: [
{
content: {
parts: [
{
text: 'Hello from Gemini!',
},
],
},
finishReason: 'STOP',
},
],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 8,
totalTokenCount: 23,
},
}
// Mock streaming response
mockGeminiStreamResponse = [
{
text: 'Hello ',
candidates: [
{
content: {
parts: [{ text: 'Hello ' }],
},
},
],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 2,
},
},
{
text: 'from ',
candidates: [
{
content: {
parts: [{ text: 'from ' }],
},
},
],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 4,
},
},
{
text: 'Gemini!',
candidates: [
{
content: {
parts: [{ text: 'Gemini!' }],
},
},
],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 8,
},
},
]
// Mock the generateContent method
;(client as any).client.models.generateContent = jest.fn().mockResolvedValue(mockGeminiResponse)
// Mock the generateContentStream method
;(client as any).client.models.generateContentStream = mockGenerateContentStream(mockGeminiStreamResponse)
})
test('basic content generation', async () => {
const response = await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
insightsProperties: { foo: 'bar' },
})
expect(response).toEqual(mockGeminiResponse)
// We expect 1 capture call
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
// Check the capture arguments
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, event, properties } = captureArgs[0]
expect(distinctId).toBe('test-id')
expect(event).toBe('$ai_generation')
expect(properties['$ai_lib']).toBe('insights-ai')
expect(properties['$ai_lib_version']).toBe(version)
expect(properties['$ai_provider']).toBe('gemini')
expect(properties['$ai_model']).toBe('gemini-2.0-flash-001')
expect(properties['$ai_input']).toEqual([{ role: 'user', content: 'Hello' }])
expect(properties['$ai_output_choices']).toEqual([
{
role: 'assistant',
content: [{ type: 'text', text: 'Hello from Gemini!' }],
},
])
expect(properties['$ai_input_tokens']).toBe(15)
expect(properties['$ai_output_tokens']).toBe(8)
expect(properties['$ai_http_status']).toBe(200)
expect(properties['foo']).toBe('bar')
expect(typeof properties['$ai_latency']).toBe('number')
// Verify raw usage metadata is passed for server-side extraction
expect(properties['$ai_usage']).toEqual({
promptTokenCount: 15,
candidatesTokenCount: 8,
totalTokenCount: 23,
})
})
test('streaming content generation', async () => {
const stream = client.models.generateContentStream({
model: 'gemini-2.0-flash-001',
contents: 'Write a short poem',
insightsDistinctId: 'test-id',
insightsProperties: { foo: 'bar' },
})
let accumulatedText = ''
for await (const chunk of stream) {
if (chunk.text) {
accumulatedText += chunk.text
}
}
expect(accumulatedText).toBe('Hello from Gemini!')
// We expect 1 capture call after streaming completes
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, event, properties } = captureArgs[0]
expect(distinctId).toBe('test-id')
expect(event).toBe('$ai_generation')
expect(properties['$ai_lib']).toBe('insights-ai')
expect(properties['$ai_lib_version']).toBe(version)
expect(properties['$ai_provider']).toBe('gemini')
expect(properties['$ai_model']).toBe('gemini-2.0-flash-001')
expect(properties['$ai_input']).toEqual([{ role: 'user', content: 'Write a short poem' }])
expect(properties['$ai_output_choices']).toEqual([
{
role: 'assistant',
content: [{ type: 'text', text: 'Hello from Gemini!' }],
},
])
expect(properties['$ai_input_tokens']).toBe(15)
expect(properties['$ai_output_tokens']).toBe(8)
expect(properties['$ai_http_status']).toBe(200)
expect(properties['foo']).toBe('bar')
expect(typeof properties['$ai_latency']).toBe('number')
// Verify raw usage metadata is passed for server-side extraction (streaming)
expect(properties['$ai_usage']).toEqual({
promptTokenCount: 15,
candidatesTokenCount: 8,
})
})
test('groups', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
insightsGroups: { team: 'ai-team' },
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { groups } = captureArgs[0]
expect(groups).toEqual({ team: 'ai-team' })
})
test('privacy mode', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Sensitive information',
insightsDistinctId: 'test-id',
insightsPrivacyMode: true,
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
expect(properties['$ai_input']).toBeNull()
expect(properties['$ai_output_choices']).toBeNull()
})
test('error handling', async () => {
const error = new Error('API Error')
;(error as any).status = 400
;(client as any).client.models.generateContent = jest.fn().mockRejectedValue(error)
await expect(
client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
})
).rejects.toThrow('API Error')
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
expect(properties['$ai_is_error']).toBe(true)
expect(properties['$ai_http_status']).toBe(400)
expect(properties['$ai_input_tokens']).toBe(0)
expect(properties['$ai_output_tokens']).toBe(0)
})
test('array contents input', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: ['Hello', 'How are you?'],
insightsDistinctId: 'test-id',
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
expect(properties['$ai_input']).toEqual([
{ role: 'user', content: 'Hello' },
{ role: 'user', content: 'How are you?' },
])
})
test('object contents input', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: { text: 'Hello world' },
insightsDistinctId: 'test-id',
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
expect(properties['$ai_input']).toEqual([{ role: 'user', content: 'Hello world' }])
})
test('capture immediate', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
insightsCaptureImmediate: true,
})
expect(mockInsightsClient.captureImmediate).toHaveBeenCalledTimes(1)
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(0)
})
test('vertex ai configuration', () => {
const vertexClient = new InsightsGemini({
vertexai: true,
project: 'test-project',
location: 'us-central1',
insights: mockInsightsClient as any,
})
expect(vertexClient).toBeInstanceOf(InsightsGemini)
expect(vertexClient.models).toBeDefined()
})
test('streaming tracks time to first token', async () => {
const stream = client.models.generateContentStream({
model: 'gemini-2.0-flash-001',
contents: 'Write a short poem',
insightsDistinctId: 'test-ttft-user',
})
for await (const _chunk of stream) {
// Just consume the stream
}
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Time to first token should be present and be a number
expect(typeof properties['$ai_time_to_first_token']).toBe('number')
expect(properties['$ai_time_to_first_token']).toBeGreaterThanOrEqual(0)
// Time to first token should be less than or equal to total latency
expect(properties['$ai_time_to_first_token']).toBeLessThanOrEqual(properties['$ai_latency'])
})
test('streaming with function calls', async () => {
// Mock streaming response with function calls
const mockStreamWithFunctions = [
{
text: 'I can help with that. ',
candidates: [
{
content: {
parts: [{ text: 'I can help with that. ' }],
},
},
],
usageMetadata: {
promptTokenCount: 20,
candidatesTokenCount: 5,
},
},
{
candidates: [
{
content: {
parts: [
{
functionCall: {
name: 'searchWeather',
args: { location: 'New York', units: 'celsius' },
},
},
],
},
},
],
usageMetadata: {
promptTokenCount: 20,
candidatesTokenCount: 10,
},
},
{
text: 'The weather is sunny.',
candidates: [
{
content: {
parts: [{ text: 'The weather is sunny.' }],
},
},
],
usageMetadata: {
promptTokenCount: 20,
candidatesTokenCount: 15,
},
},
]
// Update mock to use function call stream
;(client as any).client.models.generateContentStream = mockGenerateContentStream(mockStreamWithFunctions)
const stream = client.models.generateContentStream({
model: 'gemini-2.0-flash-001',
contents: 'What is the weather?',
insightsDistinctId: 'test-id',
})
let accumulatedText = ''
const functionCalls: any[] = []
for await (const chunk of stream) {
if (chunk.text) {
accumulatedText += chunk.text
}
if (chunk.candidates) {
for (const candidate of chunk.candidates) {
if (candidate.content?.parts) {
for (const part of candidate.content.parts) {
if ('functionCall' in part) {
functionCalls.push(part.functionCall)
}
}
}
}
}
}
expect(accumulatedText).toBe('I can help with that. The weather is sunny.')
expect(functionCalls).toHaveLength(1)
expect(functionCalls[0]).toEqual({
name: 'searchWeather',
args: { location: 'New York', units: 'celsius' },
})
// Check Insights capture
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
expect(properties['$ai_output_choices']).toEqual([
{
role: 'assistant',
content: [
{ type: 'text', text: 'I can help with that. The weather is sunny.' },
{
type: 'function',
function: {
name: 'searchWeather',
arguments: { location: 'New York', units: 'celsius' },
},
},
],
},
])
expect(properties['$ai_input_tokens']).toBe(20)
expect(properties['$ai_output_tokens']).toBe(15)
})
test('streaming with multiple text chunks accumulation', async () => {
// Mock streaming response with multiple text chunks
const mockMultipleTextChunks = [
{
text: 'The ',
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 1 },
},
{
text: 'quick ',
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 2 },
},
{
text: 'brown ',
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 3 },
},
{
text: 'fox.',
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 4 },
},
]
// Update mock
;(client as any).client.models.generateContentStream = mockGenerateContentStream(mockMultipleTextChunks)
const stream = client.models.generateContentStream({
model: 'gemini-2.0-flash-001',
contents: 'Tell me a story',
insightsDistinctId: 'test-id',
})
let accumulatedText = ''
for await (const chunk of stream) {
if (chunk.text) {
accumulatedText += chunk.text
}
}
expect(accumulatedText).toBe('The quick brown fox.')
// Check Insights capture for proper text accumulation
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Should have a single text item with all accumulated text
expect(properties['$ai_output_choices']).toEqual([
{
role: 'assistant',
content: [{ type: 'text', text: 'The quick brown fox.' }],
},
])
expect(properties['$ai_output_tokens']).toBe(4)
})
test('anonymous user - $process_person_profile set to false', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsTraceId: 'trace-123',
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, properties } = captureArgs[0]
expect(distinctId).toBe('trace-123')
expect(properties['$process_person_profile']).toBe(false)
})
test('identified user - $process_person_profile not set', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsDistinctId: 'user-456',
insightsTraceId: 'trace-123',
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, properties } = captureArgs[0]
expect(distinctId).toBe('user-456')
expect(properties['$process_person_profile']).toBeUndefined()
})
test('systemInstruction parameter as string', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'What is the weather?',
config: { systemInstruction: 'You are a helpful weather assistant.' },
insightsDistinctId: 'test-system-instruction',
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, properties } = captureArgs[0]
expect(distinctId).toBe('test-system-instruction')
expect(properties['$ai_input']).toEqual([
{ role: 'system', content: 'You are a helpful weather assistant.' },
{ role: 'user', content: 'What is the weather?' },
])
})
test('systemInstruction parameter as ContentUnion', async () => {
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'What is the capital of France?',
config: { systemInstruction: { parts: [{ text: 'You are a geography expert.' }] } },
insightsDistinctId: 'test-systemInstruction',
})
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, properties } = captureArgs[0]
expect(distinctId).toBe('test-systemInstruction')
expect(properties['$ai_input']).toEqual([
{ role: 'system', content: 'You are a geography expert.' },
{ role: 'user', content: 'What is the capital of France?' },
])
})
test('streaming with systemInstruction parameter', async () => {
const stream = client.models.generateContentStream({
model: 'gemini-2.0-flash-001',
contents: 'Tell me about AI',
config: { systemInstruction: 'You are an AI expert.' },
insightsDistinctId: 'test-stream-system',
})
for await (const _chunk of stream) {
// Just consume the stream
}
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, properties } = captureArgs[0]
expect(distinctId).toBe('test-stream-system')
expect(properties['$ai_input']).toEqual([
{ role: 'system', content: 'You are an AI expert.' },
{ role: 'user', content: 'Tell me about AI' },
])
})
describe('Web Search Tracking', () => {
test('should detect grounding metadata (binary detection)', async () => {
mockGeminiResponse = {
text: 'Based on search results, here is what I found.',
candidates: [
{
content: {
parts: [{ text: 'Based on search results, here is what I found.' }],
role: 'model',
},
finishReason: 'STOP',
index: 0,
safetyRatings: [],
groundingMetadata: {
webSearchQueries: ['Insights features'],
groundingChunks: [
{
web: {
uri: 'https://insights.hanzo.ai',
title: 'Insights',
},
},
],
},
},
],
usageMetadata: {
promptTokenCount: 25,
candidatesTokenCount: 18,
totalTokenCount: 43,
},
} as any
// Update the mock to use the new response
;(client as any).client.models.generateContent = jest.fn().mockResolvedValue(mockGeminiResponse)
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'What is Insights?',
insightsDistinctId: 'test-user',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Gemini uses binary detection (1 or 0)
expect(properties['$ai_web_search_count']).toBe(1)
})
test('should detect grounding in streaming', async () => {
// Create mock stream with grounding metadata
mockGeminiStreamResponse = [
{
text: 'Search result',
candidates: [
{
content: {
parts: [{ text: 'Search result' }],
role: 'model',
},
groundingMetadata: {
groundingChunks: [
{
web: {
uri: 'https://example.com',
title: 'Example',
},
},
],
},
},
],
usageMetadata: {
promptTokenCount: 20,
candidatesTokenCount: 10,
totalTokenCount: 30,
},
},
] as any
// Re-mock the generateContentStream method with new chunks
;(client as any).client.models.generateContentStream = mockGenerateContentStream(mockGeminiStreamResponse)
const stream = client.models.generateContentStream({
model: 'gemini-2.0-flash-001',
contents: 'Search query',
insightsDistinctId: 'test-user',
})
for await (const _chunk of stream) {
// Just consume
}
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
expect(properties['$ai_web_search_count']).toBe(1)
})
test('should return 0 for empty grounding metadata', async () => {
mockGeminiResponse = {
text: 'Regular response',
candidates: [
{
content: {
parts: [{ text: 'Regular response' }],
role: 'model',
},
finishReason: 'STOP',
index: 0,
safetyRatings: [],
groundingMetadata: null, // Explicitly null
},
],
usageMetadata: {
promptTokenCount: 15,
candidatesTokenCount: 8,
totalTokenCount: 23,
},
} as any
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Hello',
insightsDistinctId: 'test-user',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Should not include web search count when grounding not present
expect(properties['$ai_web_search_count']).toBeUndefined()
})
test('should return 0 for empty grounding arrays', async () => {
mockGeminiResponse = {
text: 'Regular response',
candidates: [
{
content: {
parts: [{ text: 'Regular response' }],
role: 'model',
},
finishReason: 'STOP',
index: 0,
safetyRatings: [],
groundingMetadata: {
webSearchQueries: [], // Empty array
groundingChunks: [], // Empty array
},
},
],
usageMetadata: {
promptTokenCount: 12,
candidatesTokenCount: 6,
totalTokenCount: 18,
},
} as any
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Test',
insightsDistinctId: 'test-user',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Empty arrays should not trigger web search count
expect(properties['$ai_web_search_count']).toBeUndefined()
})
test('should return 0 for empty grounding metadata object', async () => {
mockGeminiResponse = {
text: 'Hello! How can I help you today?',
candidates: [
{
content: {
parts: [{ text: 'Hello! How can I help you today?' }],
role: 'model',
},
finishReason: 'STOP',
groundingMetadata: {}, // Empty object - this was the bug!
index: 0,
},
],
usageMetadata: {
promptTokenCount: 2,
candidatesTokenCount: 9,
totalTokenCount: 43,
},
} as any
await client.models.generateContent({
model: 'gemini-2.5-flash',
contents: 'hi',
insightsDistinctId: 'test-user',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Empty groundingMetadata object should not trigger web search count
expect(properties['$ai_web_search_count']).toBeUndefined()
})
test('should detect google_search function call', async () => {
mockGeminiResponse = {
text: '',
candidates: [
{
content: {
parts: [
{
functionCall: {
name: 'google_search',
args: { query: 'Insights documentation' },
},
},
],
role: 'model',
},
finishReason: 'STOP',
index: 0,
safetyRatings: [],
},
],
usageMetadata: {
promptTokenCount: 30,
candidatesTokenCount: 15,
totalTokenCount: 45,
},
} as any
// Update the mock to use the new response
;(client as any).client.models.generateContent = jest.fn().mockResolvedValue(mockGeminiResponse)
await client.models.generateContent({
model: 'gemini-2.0-flash-001',
contents: 'Search for Insights',
insightsDistinctId: 'test-user',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { properties } = captureArgs[0]
// Function call with google_search should trigger web search count
expect(properties['$ai_web_search_count']).toBe(1)
})
})
describe('TTS Support', () => {
test('should support responseModalities and speechConfig in config', async () => {
mockGeminiResponse = {
candidates: [
{
content: {
parts: [
{
inlineData: {
mimeType: 'audio/wav',
data: Buffer.from('fake audio data').toString('base64'),
},
},
],
role: 'model',
},
finishReason: 'STOP',
index: 0,
safetyRatings: [],
},
],
usageMetadata: {
promptTokenCount: 10,
candidatesTokenCount: 0,
totalTokenCount: 10,
},
} as any
;(client as any).client.models.generateContent = jest.fn().mockResolvedValue(mockGeminiResponse)
await client.models.generateContent({
model: 'gemini-2.5-flash-preview-tts',
contents: [{ role: 'user', parts: [{ text: 'Say hello' }] }],
config: {
responseModalities: ['AUDIO'],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: {
voiceName: 'Kore',
},
},
},
},
insightsDistinctId: 'test-tts-user',
})
expect(mockInsightsClient.capture).toHaveBeenCalled()
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, properties } = captureArgs[0]
expect(distinctId).toBe('test-tts-user')
expect(properties['$ai_model']).toBe('gemini-2.5-flash-preview-tts')
expect(properties['$ai_input']).toEqual([{ role: 'user', content: [{ type: 'text', text: 'Say hello' }] }])
const generateContentCall = ((client as any).client.models.generateContent as jest.Mock).mock.calls[0][0]
expect(generateContentCall.config.responseModalities).toEqual(['AUDIO'])
expect(generateContentCall.config.speechConfig.voiceConfig.prebuiltVoiceConfig.voiceName).toBe('Kore')
})
})
describe('embedContent', () => {
const mockEmbedResponse = {
embeddings: [{ values: [0.1, 0.2, 0.3], statistics: null }],
}
const mockEmbedResponseWithStats = {
embeddings: [
{ values: [0.1, 0.2, 0.3], statistics: { tokenCount: 5 } },
{ values: [0.4, 0.5, 0.6], statistics: { tokenCount: 8 } },
],
}
test('basic embedding', async () => {
;(client as any).client.models.embedContent = jest.fn().mockResolvedValue(mockEmbedResponse)
const response = await client.models.embedContent({
model: 'gemini-embedding-001',
contents: 'Hello world',
insightsDistinctId: 'test-id',
insightsProperties: { foo: 'bar' },
})
expect(response).toEqual(mockEmbedResponse)
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
const { distinctId, event, properties } = captureArgs[0]
expect(distinctId).toBe('test-id')
expect(event).toBe('$ai_embedding')
expect(properties['$ai_provider']).toBe('gemini')
expect(properties['$ai_model']).toBe('gemini-embedding-001')
expect(properties['$ai_input']).toBe('Hello world')
expect(properties['$ai_output_choices']).toBeNull()
expect(properties['$ai_http_status']).toBe(200)
expect(properties['$ai_latency']).toBeGreaterThanOrEqual(0)
expect(properties['$ai_trace_id']).toBeDefined()
expect(properties['$ai_base_url']).toBe('https://generativelanguage.googleapis.com')
expect(properties['foo']).toBe('bar')
const embedCall = ((client as any).client.models.embedContent as jest.Mock).mock.calls[0][0]
expect(embedCall.model).toBe('gemini-embedding-001')
expect(embedCall.contents).toBe('Hello world')
})
test('extracts token counts from Vertex AI statistics', async () => {
;(client as any).client.models.embedContent = jest.fn().mockResolvedValue(mockEmbedResponseWithStats)
await client.models.embedContent({
model: 'gemini-embedding-001',
contents: ['Hello', 'World'],
insightsDistinctId: 'test-id',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
expect(captureArgs[0].properties['$ai_input_tokens']).toBe(13) // 5 + 8
})
test('returns 0 tokens when no statistics available', async () => {
;(client as any).client.models.embedContent = jest.fn().mockResolvedValue(mockEmbedResponse)
await client.models.embedContent({
model: 'gemini-embedding-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
expect(captureArgs[0].properties['$ai_input_tokens']).toBe(0)
})
test('privacy mode redacts input', async () => {
;(client as any).client.models.embedContent = jest.fn().mockResolvedValue(mockEmbedResponse)
await client.models.embedContent({
model: 'gemini-embedding-001',
contents: 'Secret text',
insightsDistinctId: 'test-id',
insightsPrivacyMode: true,
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
expect(captureArgs[0].properties['$ai_input']).toBeNull()
})
test('error handling captures event and rethrows', async () => {
const mockError = new Error('API error')
;(client as any).client.models.embedContent = jest.fn().mockRejectedValue(mockError)
await expect(
client.models.embedContent({
model: 'gemini-embedding-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
})
).rejects.toThrow()
expect(mockInsightsClient.capture).toHaveBeenCalledTimes(1)
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
expect(captureArgs[0].event).toBe('$ai_embedding')
expect(captureArgs[0].properties['$ai_is_error']).toBe(true)
expect(captureArgs[0].properties['$ai_input_tokens']).toBe(0)
})
test('passes config through to underlying call', async () => {
;(client as any).client.models.embedContent = jest.fn().mockResolvedValue(mockEmbedResponse)
await client.models.embedContent({
model: 'gemini-embedding-001',
contents: 'Hello',
insightsDistinctId: 'test-id',
config: { outputDimensionality: 64 },
})
const embedCall = ((client as any).client.models.embedContent as jest.Mock).mock.calls[0][0]
expect(embedCall.config).toEqual({ outputDimensionality: 64 })
})
test('no distinct id sets $process_person_profile to false', async () => {
;(client as any).client.models.embedContent = jest.fn().mockResolvedValue(mockEmbedResponse)
await client.models.embedContent({
model: 'gemini-embedding-001',
contents: 'Hello',
})
const [captureArgs] = (mockInsightsClient.capture as jest.Mock).mock.calls
expect(captureArgs[0].properties['$process_person_profile']).toBe(false)
// distinctId should fall back to traceId
expect(captureArgs[0].distinctId).toBe(captureArgs[0].properties['$ai_trace_id'])
})
})
})