mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 16:02:23 +00:00
* feat(telemetry): add generic feature_usage table and batched logging endpoint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(telemetry): log AI session usage events and document them in telemetry settings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): use escape sequence instead of literal NUL bytes in buffer key Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): validate dimensions, decouple retention, keepalive flush Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): allowlist feature-usage dimensions and index retention scans Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): pin tool-name allowlist and deploy session attribution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(telemetry): route AI chat usage through feature_usage and drop ai_chat_usage Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(telemetry): slim dimension validation to registered kinds plus key shape Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): backfill ai_chat_usage into feature_usage before dropping it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): disclose provider and model identifiers in telemetry settings text Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): issue all flush chunks before awaiting so pagehide keeps them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update ee-repo-ref to 6306c072a50937ea9af44a5bcf42345543207486 This commit updates the EE repository reference after PR #672 was merged in windmill-ee-private. Previous ee-repo-ref: 964f242a0eb44db7f7d26636cc8d76aeabea2b73 New ee-repo-ref: 6306c072a50937ea9af44a5bcf42345543207486 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('$lib/gen', () => ({ OpenAPI: { BASE: '/api' } }))
|
|
vi.mock('$lib/stores', () => ({ workspaceStore: { subscribe: () => () => {} } }))
|
|
|
|
import { createFeatureUsageBuffer, type FeatureUsageEventPayload } from './featureUsage'
|
|
|
|
describe('createFeatureUsageBuffer', () => {
|
|
it('sums repeated events per (feature, kind, key, entity) and flushes one batch', async () => {
|
|
const send = vi.fn().mockResolvedValue(undefined)
|
|
const buffer = createFeatureUsageBuffer(send, () => 'ws1')
|
|
|
|
buffer.log('ai_session', 'message', { key: 'global', entityId: 's1' })
|
|
buffer.log('ai_session', 'message', { key: 'global', entityId: 's1' })
|
|
buffer.log('ai_session', 'tokens', { entityId: 's1', value: 120 })
|
|
buffer.log('ai_session', 'message', { key: 'global', entityId: 's2' })
|
|
await buffer.flush()
|
|
|
|
expect(send).toHaveBeenCalledTimes(1)
|
|
const [workspace, events] = send.mock.calls[0]
|
|
expect(workspace).toBe('ws1')
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
{ feature: 'ai_session', kind: 'message', key: 'global', entity_id: 's1', value: 2 },
|
|
{ feature: 'ai_session', kind: 'tokens', key: '', entity_id: 's1', value: 120 },
|
|
{ feature: 'ai_session', kind: 'message', key: 'global', entity_id: 's2', value: 1 }
|
|
])
|
|
)
|
|
expect(events).toHaveLength(3)
|
|
|
|
// Flushed events must not be re-sent.
|
|
await buffer.flush()
|
|
expect(send).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('splits batches per workspace and drops events without any workspace', async () => {
|
|
const send = vi.fn().mockResolvedValue(undefined)
|
|
const buffer = createFeatureUsageBuffer(send, () => undefined)
|
|
|
|
buffer.log('ai_session', 'created', { key: 'fork' }) // no workspace -> dropped
|
|
buffer.log('ai_session', 'created', { key: 'fork', workspace: 'ws1' })
|
|
buffer.log('ai_session', 'created', { key: 'root', workspace: 'ws2' })
|
|
await buffer.flush()
|
|
|
|
expect(send).toHaveBeenCalledTimes(2)
|
|
const workspaces = send.mock.calls.map((c) => c[0]).sort()
|
|
expect(workspaces).toEqual(['ws1', 'ws2'])
|
|
})
|
|
|
|
it('starts every chunk request before any send resolves (pagehide flush)', async () => {
|
|
const send = vi.fn().mockReturnValue(new Promise<void>(() => {}))
|
|
const buffer = createFeatureUsageBuffer(send, () => 'ws1')
|
|
|
|
for (let i = 0; i < 60; i++) {
|
|
buffer.log('ai_session', 'tool', { key: `tool_${i}` })
|
|
}
|
|
buffer.log('ai_session', 'message', { workspace: 'ws2' })
|
|
void buffer.flush()
|
|
await Promise.resolve()
|
|
|
|
// keepalive only protects requests that were issued; a sequential flush
|
|
// would have started just the first chunk here.
|
|
expect(send).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('chunks flushes above the per-request cap and survives send failures', async () => {
|
|
const send = vi.fn().mockRejectedValueOnce(new Error('network')).mockResolvedValue(undefined)
|
|
const buffer = createFeatureUsageBuffer(send, () => 'ws1')
|
|
|
|
for (let i = 0; i < 60; i++) {
|
|
buffer.log('ai_session', 'tool', { key: `tool_${i}` })
|
|
}
|
|
await expect(buffer.flush()).resolves.toBeUndefined()
|
|
|
|
expect(send).toHaveBeenCalledTimes(2)
|
|
const sent = send.mock.calls.flatMap((c) => c[1] as FeatureUsageEventPayload[])
|
|
expect(send.mock.calls[0][1]).toHaveLength(50)
|
|
expect(sent).toHaveLength(60)
|
|
})
|
|
})
|