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
395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
jest.mock('server-only', () => ({}))
|
|
|
|
// Mock insights-node
|
|
const mockCapture = jest.fn()
|
|
const mockIdentify = jest.fn()
|
|
const mockIsFeatureEnabled = jest.fn()
|
|
const mockGetFeatureFlag = jest.fn()
|
|
const mockGetFeatureFlagPayload = jest.fn()
|
|
const mockGetAllFlags = jest.fn()
|
|
const mockGetAllFlagsAndPayloads = jest.fn()
|
|
const mockShutdown = jest.fn()
|
|
const mockEnterContext = jest.fn()
|
|
const mockWithContext = jest.fn((_, fn) => fn())
|
|
|
|
jest.mock('@hanzo/insights-node', () => ({
|
|
Insights: jest.fn().mockImplementation(() => ({
|
|
capture: mockCapture,
|
|
identify: mockIdentify,
|
|
isFeatureEnabled: mockIsFeatureEnabled,
|
|
getFeatureFlag: mockGetFeatureFlag,
|
|
getFeatureFlagPayload: mockGetFeatureFlagPayload,
|
|
getAllFlags: mockGetAllFlags,
|
|
getAllFlagsAndPayloads: mockGetAllFlagsAndPayloads,
|
|
shutdown: mockShutdown,
|
|
enterContext: mockEnterContext,
|
|
withContext: mockWithContext,
|
|
})),
|
|
}))
|
|
|
|
// Mock next/headers cookies()
|
|
function createMockCookies(entries: Record<string, string>) {
|
|
return {
|
|
get: jest.fn((name: string) => {
|
|
const value = entries[name]
|
|
return value !== undefined ? { name, value } : undefined
|
|
}),
|
|
getAll: jest.fn(() => Object.entries(entries).map(([name, value]) => ({ name, value }))),
|
|
has: jest.fn((name: string) => name in entries),
|
|
}
|
|
}
|
|
|
|
const mockCookieStore = createMockCookies({})
|
|
|
|
function createMockHeaders(entries: Record<string, string>) {
|
|
return {
|
|
get: jest.fn((name: string) => entries[name.toLowerCase()] ?? null),
|
|
}
|
|
}
|
|
|
|
const mockHeaderStore = createMockHeaders({})
|
|
|
|
jest.mock('next/headers.js', () => ({
|
|
cookies: jest.fn(() => Promise.resolve(mockCookieStore)),
|
|
headers: jest.fn(() => Promise.resolve(mockHeaderStore)),
|
|
}))
|
|
|
|
// Mock nodeClientCache to avoid cross-test cache pollution
|
|
const mockGetOrCreateNodeClient = jest.fn().mockImplementation(() => ({
|
|
capture: mockCapture,
|
|
identify: mockIdentify,
|
|
isFeatureEnabled: mockIsFeatureEnabled,
|
|
getFeatureFlag: mockGetFeatureFlag,
|
|
getFeatureFlagPayload: mockGetFeatureFlagPayload,
|
|
getAllFlags: mockGetAllFlags,
|
|
getAllFlagsAndPayloads: mockGetAllFlagsAndPayloads,
|
|
shutdown: mockShutdown,
|
|
enterContext: mockEnterContext,
|
|
withContext: mockWithContext,
|
|
}))
|
|
|
|
jest.mock('../src/server/nodeClientCache', () => ({
|
|
getOrCreateNodeClient: (...args: unknown[]) => mockGetOrCreateNodeClient(...args),
|
|
}))
|
|
|
|
import { getInsights } from '../src/server/getInsights'
|
|
import { cookies, headers } from 'next/headers.js'
|
|
|
|
describe('getInsights', () => {
|
|
const originalEnv = process.env
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
process.env = { ...originalEnv }
|
|
process.env.NEXT_PUBLIC_INSIGHTS_KEY = 'phc_env_key'
|
|
|
|
// Reset to empty cookies and headers by default
|
|
const emptyCookies = createMockCookies({})
|
|
;(cookies as jest.Mock).mockResolvedValue(emptyCookies)
|
|
const emptyHeaders = createMockHeaders({})
|
|
;(headers as jest.Mock).mockResolvedValue(emptyHeaders)
|
|
})
|
|
|
|
afterAll(() => {
|
|
process.env = originalEnv
|
|
})
|
|
|
|
it('returns an IInsights instance', async () => {
|
|
const client = await getInsights('phc_test123')
|
|
|
|
expect(client).toBeDefined()
|
|
expect(typeof client.capture).toBe('function')
|
|
expect(typeof client.isFeatureEnabled).toBe('function')
|
|
})
|
|
|
|
it('wraps method calls with withContext using cookie identity', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'user_abc',
|
|
$device_id: 'device_xyz',
|
|
$sesid: [1708700000000, 'session-123', 1708700000000],
|
|
}),
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'user_abc', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: 'user_abc',
|
|
sessionId: 'session-123',
|
|
properties: { $session_id: 'session-123', $device_id: 'device_xyz' },
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
expect(mockCapture).toHaveBeenCalledWith({ distinctId: 'user_abc', event: 'test_event' })
|
|
})
|
|
|
|
it('wraps method calls with withContext with undefined identity when no cookie exists', async () => {
|
|
const cookieStore = createMockCookies({})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'anon', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: undefined,
|
|
sessionId: undefined,
|
|
properties: undefined,
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
|
|
it('uses explicit apiKey over env var', async () => {
|
|
process.env.NEXT_PUBLIC_INSIGHTS_KEY = 'phc_env_key'
|
|
|
|
await getInsights('phc_explicit_key')
|
|
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('phc_explicit_key', {
|
|
host: 'https://insights.hanzo.ai',
|
|
})
|
|
})
|
|
|
|
it('falls back to NEXT_PUBLIC_INSIGHTS_KEY env var when no apiKey provided', async () => {
|
|
process.env.NEXT_PUBLIC_INSIGHTS_KEY = 'phc_env_key'
|
|
|
|
await getInsights()
|
|
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('phc_env_key', {
|
|
host: 'https://insights.hanzo.ai',
|
|
})
|
|
})
|
|
|
|
it('warns and returns a disabled client when no apiKey provided and env var missing', async () => {
|
|
delete process.env.NEXT_PUBLIC_INSIGHTS_KEY
|
|
const warnSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
|
|
const client = await getInsights()
|
|
|
|
expect(client).toBeDefined()
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('', {
|
|
host: 'https://insights.hanzo.ai',
|
|
})
|
|
expect(cookies).not.toHaveBeenCalled()
|
|
expect(headers).not.toHaveBeenCalled()
|
|
expect(warnSpy).toHaveBeenCalledWith('[Insights Next.js] apiKey is required — Insights will not be initialized')
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('passes host from options to getOrCreateNodeClient', async () => {
|
|
await getInsights('phc_test123', { host: 'https://custom.insights.example.com' })
|
|
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('phc_test123', {
|
|
host: 'https://custom.insights.example.com',
|
|
})
|
|
})
|
|
|
|
it('defaults host when it is omitted', async () => {
|
|
await getInsights('phc_test123')
|
|
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('phc_test123', {
|
|
host: 'https://insights.hanzo.ai',
|
|
})
|
|
})
|
|
|
|
it('trims apiKey and host before creating the node client', async () => {
|
|
await getInsights(' phc_test123\n', { host: ' https://custom.insights.example.com/\t ' })
|
|
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('phc_test123', {
|
|
host: 'https://custom.insights.example.com/',
|
|
})
|
|
})
|
|
|
|
it('reads host from NEXT_PUBLIC_INSIGHTS_HOST env var when not in options', async () => {
|
|
process.env.NEXT_PUBLIC_INSIGHTS_HOST = 'https://env-host.insights.example.com'
|
|
|
|
await getInsights('phc_test123')
|
|
|
|
expect(mockGetOrCreateNodeClient).toHaveBeenCalledWith('phc_test123', {
|
|
host: 'https://env-host.insights.example.com',
|
|
})
|
|
|
|
delete process.env.NEXT_PUBLIC_INSIGHTS_HOST
|
|
})
|
|
|
|
it('wraps method calls with withContext with just distinctId when cookie has no session or device', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'user_abc',
|
|
}),
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'user_abc', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: 'user_abc',
|
|
sessionId: undefined,
|
|
properties: undefined,
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
|
|
describe('tracing headers', () => {
|
|
it('uses tracing headers when present and no cookie exists', async () => {
|
|
const cookieStore = createMockCookies({})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
const headerStore = createMockHeaders({
|
|
'x-insights-session-id': 'header-session-456',
|
|
'x-insights-distinct-id': 'header-user-789',
|
|
'x-insights-window-id': 'window-abc',
|
|
})
|
|
;(headers as jest.Mock).mockResolvedValue(headerStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'header-user-789', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: 'header-user-789',
|
|
sessionId: 'header-session-456',
|
|
properties: {
|
|
$session_id: 'header-session-456',
|
|
$window_id: 'window-abc',
|
|
},
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
|
|
it('tracing headers override cookie values for distinctId and sessionId', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'cookie-user',
|
|
$device_id: 'device_xyz',
|
|
$sesid: [1708700000000, 'cookie-session', 1708700000000],
|
|
}),
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
const headerStore = createMockHeaders({
|
|
'x-insights-session-id': 'header-session',
|
|
'x-insights-distinct-id': 'header-user',
|
|
})
|
|
;(headers as jest.Mock).mockResolvedValue(headerStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'header-user', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: 'header-user',
|
|
sessionId: 'header-session',
|
|
properties: {
|
|
$session_id: 'header-session',
|
|
$device_id: 'device_xyz',
|
|
},
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
|
|
it('falls back to cookie values when tracing headers are absent', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'cookie-user',
|
|
$device_id: 'device_xyz',
|
|
$sesid: [1708700000000, 'cookie-session', 1708700000000],
|
|
}),
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
const headerStore = createMockHeaders({})
|
|
;(headers as jest.Mock).mockResolvedValue(headerStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'cookie-user', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: 'cookie-user',
|
|
sessionId: 'cookie-session',
|
|
properties: {
|
|
$session_id: 'cookie-session',
|
|
$device_id: 'device_xyz',
|
|
},
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
|
|
it('adds $window_id from tracing headers alongside cookie properties', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'cookie-user',
|
|
$device_id: 'device_xyz',
|
|
$sesid: [1708700000000, 'cookie-session', 1708700000000],
|
|
}),
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
const headerStore = createMockHeaders({
|
|
'x-insights-window-id': 'window-123',
|
|
})
|
|
;(headers as jest.Mock).mockResolvedValue(headerStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'cookie-user', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
{
|
|
distinctId: 'cookie-user',
|
|
sessionId: 'cookie-session',
|
|
properties: {
|
|
$session_id: 'cookie-session',
|
|
$device_id: 'device_xyz',
|
|
$window_id: 'window-123',
|
|
},
|
|
},
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('consent awareness', () => {
|
|
it('returns the client directly without proxy when consent cookie is 0', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'user_abc',
|
|
$device_id: 'device_xyz',
|
|
}),
|
|
__ph_opt_in_out_phc_test123: '0',
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
expect(client).toBeDefined()
|
|
// When opted out, methods should not go through withContext
|
|
client.capture({ distinctId: 'user_abc', event: 'test_event' })
|
|
expect(mockWithContext).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('wraps method calls with withContext when consent cookie is 1', async () => {
|
|
const cookieStore = createMockCookies({
|
|
ph_phc_test123_insights: JSON.stringify({
|
|
distinct_id: 'user_abc',
|
|
$device_id: 'device_xyz',
|
|
}),
|
|
__ph_opt_in_out_phc_test123: '1',
|
|
})
|
|
;(cookies as jest.Mock).mockResolvedValue(cookieStore)
|
|
|
|
const client = await getInsights('phc_test123')
|
|
client.capture({ distinctId: 'user_abc', event: 'test_event' })
|
|
|
|
expect(mockWithContext).toHaveBeenCalledWith(
|
|
expect.objectContaining({ distinctId: 'user_abc' }),
|
|
expect.any(Function)
|
|
)
|
|
})
|
|
})
|
|
})
|