@illuma-ai/observability-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/core-client.test.d.ts +8 -0
- package/dist/__tests__/core-client.test.d.ts.map +1 -0
- package/dist/__tests__/core-client.test.js +844 -0
- package/dist/__tests__/core-client.test.js.map +1 -0
- package/dist/__tests__/generation-client.test.d.ts +8 -0
- package/dist/__tests__/generation-client.test.d.ts.map +1 -0
- package/dist/__tests__/generation-client.test.js +117 -0
- package/dist/__tests__/generation-client.test.js.map +1 -0
- package/dist/__tests__/span-client.test.d.ts +9 -0
- package/dist/__tests__/span-client.test.d.ts.map +1 -0
- package/dist/__tests__/span-client.test.js +272 -0
- package/dist/__tests__/span-client.test.js.map +1 -0
- package/dist/__tests__/trace-client.test.d.ts +9 -0
- package/dist/__tests__/trace-client.test.d.ts.map +1 -0
- package/dist/__tests__/trace-client.test.js +260 -0
- package/dist/__tests__/trace-client.test.js.map +1 -0
- package/dist/__tests__/types.test.d.ts +9 -0
- package/dist/__tests__/types.test.d.ts.map +1 -0
- package/dist/__tests__/types.test.js +453 -0
- package/dist/__tests__/types.test.js.map +1 -0
- package/dist/__tests__/utils.test.d.ts +7 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.js +164 -0
- package/dist/__tests__/utils.test.js.map +1 -0
- package/dist/core-client.d.ts +1 -0
- package/dist/core-client.d.ts.map +1 -1
- package/dist/core-client.js +4 -0
- package/dist/core-client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/span-client.d.ts +28 -0
- package/dist/span-client.d.ts.map +1 -1
- package/dist/span-client.js +92 -0
- package/dist/span-client.js.map +1 -1
- package/dist/trace-client.d.ts +35 -0
- package/dist/trace-client.d.ts.map +1 -1
- package/dist/trace-client.js +110 -0
- package/dist/trace-client.js.map +1 -1
- package/dist/types.d.ts +109 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +40 -2
- package/dist/types.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ObservabilityCoreClient.
|
|
3
|
+
*
|
|
4
|
+
* Validates event batching, auto-flush, manual flush, shutdown behavior,
|
|
5
|
+
* and the TraceClient -> SpanClient -> GenerationClient chaining API.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { ObservabilityCoreClient, IngestionEventType, } from '../index.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Concrete test implementation of the abstract core client
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
class TestClient extends ObservabilityCoreClient {
|
|
13
|
+
/** Captured fetch calls for assertions. */
|
|
14
|
+
fetchCalls = [];
|
|
15
|
+
/** Configurable mock response returned by fetchWithRetry. */
|
|
16
|
+
mockResponse = {
|
|
17
|
+
status: 200,
|
|
18
|
+
statusText: 'OK',
|
|
19
|
+
ok: true,
|
|
20
|
+
json: async () => ({ successes: [], errors: [] }),
|
|
21
|
+
text: async () => '',
|
|
22
|
+
};
|
|
23
|
+
async fetchWithRetry(url, options) {
|
|
24
|
+
this.fetchCalls.push({ url, options });
|
|
25
|
+
return this.mockResponse;
|
|
26
|
+
}
|
|
27
|
+
/** Helper to inspect the last request body sent. */
|
|
28
|
+
getLastRequestBody() {
|
|
29
|
+
const last = this.fetchCalls.at(-1);
|
|
30
|
+
if (!last?.options.body)
|
|
31
|
+
return undefined;
|
|
32
|
+
return JSON.parse(last.options.body);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
function createClient(overrides = {}) {
|
|
39
|
+
return new TestClient({
|
|
40
|
+
publicKey: 'pk-test',
|
|
41
|
+
secretKey: 'sk-test',
|
|
42
|
+
baseUrl: 'https://observe.test',
|
|
43
|
+
flushAt: 5,
|
|
44
|
+
flushInterval: 0, // disable periodic flush timer in tests
|
|
45
|
+
...overrides,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Tests
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
describe('ObservabilityCoreClient', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.useFakeTimers();
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
vi.useRealTimers();
|
|
57
|
+
});
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
// Event batching
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
61
|
+
describe('event batching', () => {
|
|
62
|
+
it('should enqueue events into the internal queue', () => {
|
|
63
|
+
const client = createClient();
|
|
64
|
+
client.enqueue({
|
|
65
|
+
id: 'evt-1',
|
|
66
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
67
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
68
|
+
body: { id: 'trace-1' },
|
|
69
|
+
});
|
|
70
|
+
client.enqueue({
|
|
71
|
+
id: 'evt-2',
|
|
72
|
+
type: IngestionEventType.GENERATION_CREATE,
|
|
73
|
+
timestamp: '2024-01-01T00:00:01.000Z',
|
|
74
|
+
body: { id: 'gen-1', traceId: 'trace-1' },
|
|
75
|
+
});
|
|
76
|
+
const queue = client.getQueue();
|
|
77
|
+
expect(queue).toHaveLength(2);
|
|
78
|
+
expect(queue[0].id).toBe('evt-1');
|
|
79
|
+
expect(queue[1].id).toBe('evt-2');
|
|
80
|
+
});
|
|
81
|
+
it('should include all event fields in the batch payload', async () => {
|
|
82
|
+
const client = createClient({ flushAt: 100 });
|
|
83
|
+
client.enqueue({
|
|
84
|
+
id: 'evt-1',
|
|
85
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
86
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
87
|
+
body: { id: 'trace-1', name: 'test-trace', userId: 'user-42' },
|
|
88
|
+
});
|
|
89
|
+
await client.flush();
|
|
90
|
+
const body = client.getLastRequestBody();
|
|
91
|
+
expect(body).toBeDefined();
|
|
92
|
+
expect(body.batch).toHaveLength(1);
|
|
93
|
+
expect(body.batch[0].type).toBe('trace-create');
|
|
94
|
+
expect(body.batch[0].body).toMatchObject({
|
|
95
|
+
id: 'trace-1',
|
|
96
|
+
name: 'test-trace',
|
|
97
|
+
userId: 'user-42',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// -------------------------------------------------------------------------
|
|
102
|
+
// Auto-flush at threshold
|
|
103
|
+
// -------------------------------------------------------------------------
|
|
104
|
+
describe('auto-flush at threshold', () => {
|
|
105
|
+
it('should auto-flush when queue reaches flushAt', async () => {
|
|
106
|
+
const client = createClient({ flushAt: 3 });
|
|
107
|
+
// Enqueue 3 events to hit the threshold
|
|
108
|
+
for (let i = 0; i < 3; i++) {
|
|
109
|
+
client.enqueue({
|
|
110
|
+
id: `evt-${i}`,
|
|
111
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
112
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
113
|
+
body: { id: `trace-${i}` },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// flush() is called asynchronously — allow microtasks to settle
|
|
117
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
118
|
+
expect(client.fetchCalls).toHaveLength(1);
|
|
119
|
+
const body = client.getLastRequestBody();
|
|
120
|
+
expect(body.batch).toHaveLength(3);
|
|
121
|
+
// Queue should be empty after flush
|
|
122
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it('should not flush before reaching the threshold', () => {
|
|
125
|
+
const client = createClient({ flushAt: 5 });
|
|
126
|
+
for (let i = 0; i < 4; i++) {
|
|
127
|
+
client.enqueue({
|
|
128
|
+
id: `evt-${i}`,
|
|
129
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
130
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
131
|
+
body: { id: `trace-${i}` },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
expect(client.fetchCalls).toHaveLength(0);
|
|
135
|
+
expect(client.getQueue()).toHaveLength(4);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
// Manual flush
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
describe('manual flush', () => {
|
|
142
|
+
it('should flush all queued events when called explicitly', async () => {
|
|
143
|
+
const client = createClient({ flushAt: 100 });
|
|
144
|
+
client.enqueue({
|
|
145
|
+
id: 'evt-1',
|
|
146
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
147
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
148
|
+
body: { id: 'trace-1' },
|
|
149
|
+
});
|
|
150
|
+
client.enqueue({
|
|
151
|
+
id: 'evt-2',
|
|
152
|
+
type: IngestionEventType.SPAN_CREATE,
|
|
153
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
154
|
+
body: { id: 'span-1', traceId: 'trace-1' },
|
|
155
|
+
});
|
|
156
|
+
await client.flush();
|
|
157
|
+
expect(client.fetchCalls).toHaveLength(1);
|
|
158
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
it('should be a no-op when the queue is empty', async () => {
|
|
161
|
+
const client = createClient();
|
|
162
|
+
await client.flush();
|
|
163
|
+
expect(client.fetchCalls).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
it('should send to the correct ingestion endpoint', async () => {
|
|
166
|
+
const client = createClient({ baseUrl: 'https://my-server.example.com' });
|
|
167
|
+
client.enqueue({
|
|
168
|
+
id: 'evt-1',
|
|
169
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
170
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
171
|
+
body: { id: 'trace-1' },
|
|
172
|
+
});
|
|
173
|
+
await client.flush();
|
|
174
|
+
expect(client.fetchCalls[0].url).toBe('https://my-server.example.com/api/public/ingestion');
|
|
175
|
+
});
|
|
176
|
+
it('should include Authorization header with Basic auth', async () => {
|
|
177
|
+
const client = createClient({
|
|
178
|
+
publicKey: 'pk-abc',
|
|
179
|
+
secretKey: 'sk-xyz',
|
|
180
|
+
});
|
|
181
|
+
client.enqueue({
|
|
182
|
+
id: 'evt-1',
|
|
183
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
184
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
185
|
+
body: { id: 'trace-1' },
|
|
186
|
+
});
|
|
187
|
+
await client.flush();
|
|
188
|
+
const authHeader = client.fetchCalls[0].options.headers['Authorization'];
|
|
189
|
+
// Basic base64("pk-abc:sk-xyz")
|
|
190
|
+
const expected = `Basic ${Buffer.from('pk-abc:sk-xyz').toString('base64')}`;
|
|
191
|
+
expect(authHeader).toBe(expected);
|
|
192
|
+
});
|
|
193
|
+
it('should include SDK metadata in the request body', async () => {
|
|
194
|
+
const client = createClient();
|
|
195
|
+
client.enqueue({
|
|
196
|
+
id: 'evt-1',
|
|
197
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
198
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
199
|
+
body: { id: 'trace-1' },
|
|
200
|
+
});
|
|
201
|
+
await client.flush();
|
|
202
|
+
const body = client.getLastRequestBody();
|
|
203
|
+
expect(body.metadata).toBeDefined();
|
|
204
|
+
expect(body.metadata.publicKey).toBe('pk-test');
|
|
205
|
+
expect(body.metadata.sdkName).toBe('@illuma-ai/observability-core');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
// Shutdown flushes remaining events
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
describe('shutdown', () => {
|
|
212
|
+
it('should flush remaining events on shutdown', async () => {
|
|
213
|
+
const client = createClient({ flushAt: 100 });
|
|
214
|
+
client.enqueue({
|
|
215
|
+
id: 'evt-1',
|
|
216
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
217
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
218
|
+
body: { id: 'trace-1' },
|
|
219
|
+
});
|
|
220
|
+
client.enqueue({
|
|
221
|
+
id: 'evt-2',
|
|
222
|
+
type: IngestionEventType.GENERATION_CREATE,
|
|
223
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
224
|
+
body: { id: 'gen-1' },
|
|
225
|
+
});
|
|
226
|
+
await client.shutdown();
|
|
227
|
+
expect(client.fetchCalls).toHaveLength(1);
|
|
228
|
+
const body = client.getLastRequestBody();
|
|
229
|
+
expect(body.batch).toHaveLength(2);
|
|
230
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
231
|
+
});
|
|
232
|
+
it('should be safe to call shutdown with an empty queue', async () => {
|
|
233
|
+
const client = createClient();
|
|
234
|
+
await client.shutdown();
|
|
235
|
+
expect(client.fetchCalls).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
it('should clear the flush timer on shutdown', async () => {
|
|
238
|
+
// Enable periodic flush to test timer clearing
|
|
239
|
+
const client = createClient({ flushInterval: 5000 });
|
|
240
|
+
await client.shutdown();
|
|
241
|
+
// Advancing time should not trigger any additional flushes
|
|
242
|
+
client.enqueue({
|
|
243
|
+
id: 'evt-late',
|
|
244
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
245
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
246
|
+
body: { id: 'trace-late' },
|
|
247
|
+
});
|
|
248
|
+
vi.advanceTimersByTime(10_000);
|
|
249
|
+
// Give any pending promises time to resolve
|
|
250
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
251
|
+
// The event should still be in the queue (timer was cleared)
|
|
252
|
+
expect(client.getQueue()).toHaveLength(1);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
// -------------------------------------------------------------------------
|
|
256
|
+
// Disabled client
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
describe('disabled client', () => {
|
|
259
|
+
it('should not enqueue events when disabled', () => {
|
|
260
|
+
const client = createClient({ enabled: false });
|
|
261
|
+
client.enqueue({
|
|
262
|
+
id: 'evt-1',
|
|
263
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
264
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
265
|
+
body: { id: 'trace-1' },
|
|
266
|
+
});
|
|
267
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
268
|
+
});
|
|
269
|
+
it('should return a TraceClient even when disabled', () => {
|
|
270
|
+
const client = createClient({ enabled: false });
|
|
271
|
+
const trace = client.trace({ name: 'noop-trace' });
|
|
272
|
+
expect(trace).toBeDefined();
|
|
273
|
+
expect(trace.id).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
// TraceClient chaining: trace -> generation -> score
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
describe('TraceClient chaining', () => {
|
|
280
|
+
it('should create trace, generation, and score events via chaining', async () => {
|
|
281
|
+
const client = createClient({ flushAt: 100 });
|
|
282
|
+
const trace = client.trace({ name: 'chat-workflow' });
|
|
283
|
+
const gen = trace.generation({
|
|
284
|
+
name: 'llm-call',
|
|
285
|
+
model: 'gpt-4o',
|
|
286
|
+
input: [{ role: 'user', content: 'Hello' }],
|
|
287
|
+
});
|
|
288
|
+
gen.end({
|
|
289
|
+
output: { role: 'assistant', content: 'Hi!' },
|
|
290
|
+
usage: { input: 10, output: 5, total: 15 },
|
|
291
|
+
});
|
|
292
|
+
trace.score({ name: 'quality', value: 0.95 });
|
|
293
|
+
await client.flush();
|
|
294
|
+
const body = client.getLastRequestBody();
|
|
295
|
+
expect(body).toBeDefined();
|
|
296
|
+
const types = body.batch.map((e) => e.type);
|
|
297
|
+
expect(types).toContain('trace-create');
|
|
298
|
+
expect(types).toContain('generation-create');
|
|
299
|
+
expect(types).toContain('generation-update');
|
|
300
|
+
expect(types).toContain('score-create');
|
|
301
|
+
// Verify trace event
|
|
302
|
+
const traceEvent = body.batch.find((e) => e.type === 'trace-create');
|
|
303
|
+
expect(traceEvent.body.name).toBe('chat-workflow');
|
|
304
|
+
// Verify generation-create has correct model and traceId
|
|
305
|
+
const genCreate = body.batch.find((e) => e.type === 'generation-create');
|
|
306
|
+
expect(genCreate.body.model).toBe('gpt-4o');
|
|
307
|
+
expect(genCreate.body.traceId).toBe(trace.id);
|
|
308
|
+
// Verify generation-update (from .end()) has usage
|
|
309
|
+
const genUpdate = body.batch.find((e) => e.type === 'generation-update');
|
|
310
|
+
expect(genUpdate.body.usage).toMatchObject({
|
|
311
|
+
input: 10,
|
|
312
|
+
output: 5,
|
|
313
|
+
total: 15,
|
|
314
|
+
});
|
|
315
|
+
// Verify score is attached to the trace
|
|
316
|
+
const scoreEvent = body.batch.find((e) => e.type === 'score-create');
|
|
317
|
+
expect(scoreEvent.body.name).toBe('quality');
|
|
318
|
+
expect(scoreEvent.body.value).toBe(0.95);
|
|
319
|
+
expect(scoreEvent.body.traceId).toBe(trace.id);
|
|
320
|
+
});
|
|
321
|
+
it('should support trace.update() for adding output after the fact', async () => {
|
|
322
|
+
const client = createClient({ flushAt: 100 });
|
|
323
|
+
const trace = client.trace({ name: 'workflow' });
|
|
324
|
+
trace.update({ output: 'final result', tags: ['test'] });
|
|
325
|
+
await client.flush();
|
|
326
|
+
const body = client.getLastRequestBody();
|
|
327
|
+
// Should have 2 trace-create events (initial + update/upsert)
|
|
328
|
+
const traceEvents = body.batch.filter((e) => e.type === 'trace-create');
|
|
329
|
+
expect(traceEvents).toHaveLength(2);
|
|
330
|
+
const updateEvent = traceEvents[1];
|
|
331
|
+
expect(updateEvent.body.output).toBe('final result');
|
|
332
|
+
expect(updateEvent.body.tags).toEqual(['test']);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
// -------------------------------------------------------------------------
|
|
336
|
+
// SpanClient nesting
|
|
337
|
+
// -------------------------------------------------------------------------
|
|
338
|
+
describe('SpanClient nesting', () => {
|
|
339
|
+
it('should support nested spans with correct parentObservationId', async () => {
|
|
340
|
+
const client = createClient({ flushAt: 100 });
|
|
341
|
+
const trace = client.trace({ name: 'pipeline' });
|
|
342
|
+
const outerSpan = trace.span({ name: 'retrieval' });
|
|
343
|
+
const innerSpan = outerSpan.span({ name: 'vector-search' });
|
|
344
|
+
innerSpan.end({ output: { results: 5 } });
|
|
345
|
+
outerSpan.end();
|
|
346
|
+
await client.flush();
|
|
347
|
+
const body = client.getLastRequestBody();
|
|
348
|
+
const spanCreates = body.batch.filter((e) => e.type === 'span-create');
|
|
349
|
+
expect(spanCreates).toHaveLength(2);
|
|
350
|
+
// Outer span: parent is the trace (no parentObservationId, just traceId)
|
|
351
|
+
const outer = spanCreates.find((e) => e.body.name === 'retrieval');
|
|
352
|
+
expect(outer.body.traceId).toBe(trace.id);
|
|
353
|
+
expect(outer.body.parentObservationId).toBeUndefined();
|
|
354
|
+
// Inner span: parentObservationId should be the outer span's ID
|
|
355
|
+
const inner = spanCreates.find((e) => e.body.name === 'vector-search');
|
|
356
|
+
expect(inner.body.traceId).toBe(trace.id);
|
|
357
|
+
expect(inner.body.parentObservationId).toBe(outerSpan.id);
|
|
358
|
+
});
|
|
359
|
+
it('should support creating generations under a span', async () => {
|
|
360
|
+
const client = createClient({ flushAt: 100 });
|
|
361
|
+
const trace = client.trace({ name: 'agent' });
|
|
362
|
+
const span = trace.span({ name: 'tool-call' });
|
|
363
|
+
const gen = span.generation({
|
|
364
|
+
name: 'llm',
|
|
365
|
+
model: 'claude-3-opus',
|
|
366
|
+
});
|
|
367
|
+
gen.end({ output: 'response text' });
|
|
368
|
+
await client.flush();
|
|
369
|
+
const body = client.getLastRequestBody();
|
|
370
|
+
const genCreate = body.batch.find((e) => e.type === 'generation-create');
|
|
371
|
+
expect(genCreate.body.traceId).toBe(trace.id);
|
|
372
|
+
expect(genCreate.body.parentObservationId).toBe(span.id);
|
|
373
|
+
expect(genCreate.body.model).toBe('claude-3-opus');
|
|
374
|
+
});
|
|
375
|
+
it('should support events under a span', async () => {
|
|
376
|
+
const client = createClient({ flushAt: 100 });
|
|
377
|
+
const trace = client.trace({ name: 'workflow' });
|
|
378
|
+
const span = trace.span({ name: 'processing' });
|
|
379
|
+
span.event({ name: 'cache-hit', metadata: { key: 'doc-123' } });
|
|
380
|
+
await client.flush();
|
|
381
|
+
const body = client.getLastRequestBody();
|
|
382
|
+
const eventCreate = body.batch.find((e) => e.type === 'event-create');
|
|
383
|
+
expect(eventCreate.body.traceId).toBe(trace.id);
|
|
384
|
+
expect(eventCreate.body.parentObservationId).toBe(span.id);
|
|
385
|
+
expect(eventCreate.body.name).toBe('cache-hit');
|
|
386
|
+
});
|
|
387
|
+
it('should support scoring a span', async () => {
|
|
388
|
+
const client = createClient({ flushAt: 100 });
|
|
389
|
+
const trace = client.trace({ name: 'workflow' });
|
|
390
|
+
const span = trace.span({ name: 'retrieval' });
|
|
391
|
+
span.score({ name: 'relevance', value: 0.8 });
|
|
392
|
+
await client.flush();
|
|
393
|
+
const body = client.getLastRequestBody();
|
|
394
|
+
const scoreEvent = body.batch.find((e) => e.type === 'score-create');
|
|
395
|
+
expect(scoreEvent.body.traceId).toBe(trace.id);
|
|
396
|
+
expect(scoreEvent.body.observationId).toBe(span.id);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
// -------------------------------------------------------------------------
|
|
400
|
+
// GenerationClient
|
|
401
|
+
// -------------------------------------------------------------------------
|
|
402
|
+
describe('GenerationClient', () => {
|
|
403
|
+
it('should create a generation with model and usage params', async () => {
|
|
404
|
+
const client = createClient({ flushAt: 100 });
|
|
405
|
+
const trace = client.trace({ name: 'llm-call' });
|
|
406
|
+
trace.generation({
|
|
407
|
+
name: 'completion',
|
|
408
|
+
model: 'gpt-4o-mini',
|
|
409
|
+
input: { messages: [{ role: 'user', content: 'Hi' }] },
|
|
410
|
+
output: { role: 'assistant', content: 'Hello!' },
|
|
411
|
+
usage: { input: 5, output: 3, total: 8, unit: 'TOKENS' },
|
|
412
|
+
modelParameters: { temperature: 0.7, max_tokens: 100 },
|
|
413
|
+
});
|
|
414
|
+
await client.flush();
|
|
415
|
+
const body = client.getLastRequestBody();
|
|
416
|
+
const gen = body.batch.find((e) => e.type === 'generation-create');
|
|
417
|
+
expect(gen.body.model).toBe('gpt-4o-mini');
|
|
418
|
+
expect(gen.body.usage).toMatchObject({
|
|
419
|
+
input: 5,
|
|
420
|
+
output: 3,
|
|
421
|
+
total: 8,
|
|
422
|
+
unit: 'TOKENS',
|
|
423
|
+
});
|
|
424
|
+
expect(gen.body.modelParameters).toMatchObject({
|
|
425
|
+
temperature: 0.7,
|
|
426
|
+
max_tokens: 100,
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
it('should support update() for streaming completions', async () => {
|
|
430
|
+
const client = createClient({ flushAt: 100 });
|
|
431
|
+
const trace = client.trace({ name: 'stream' });
|
|
432
|
+
const gen = trace.generation({
|
|
433
|
+
name: 'streaming-completion',
|
|
434
|
+
model: 'gpt-4o',
|
|
435
|
+
input: 'prompt',
|
|
436
|
+
});
|
|
437
|
+
// Simulate streaming: update with output and usage after completion
|
|
438
|
+
gen.update({
|
|
439
|
+
output: 'streamed response',
|
|
440
|
+
usage: { input: 20, output: 50, total: 70 },
|
|
441
|
+
});
|
|
442
|
+
await client.flush();
|
|
443
|
+
const body = client.getLastRequestBody();
|
|
444
|
+
const genUpdate = body.batch.find((e) => e.type === 'generation-update');
|
|
445
|
+
expect(genUpdate.body.output).toBe('streamed response');
|
|
446
|
+
expect(genUpdate.body.id).toBe(gen.id);
|
|
447
|
+
});
|
|
448
|
+
it('should support scoring a generation', async () => {
|
|
449
|
+
const client = createClient({ flushAt: 100 });
|
|
450
|
+
const trace = client.trace({ name: 'eval' });
|
|
451
|
+
const gen = trace.generation({ model: 'gpt-4o' });
|
|
452
|
+
gen.score({ name: 'accuracy', value: 1.0, comment: 'Correct answer' });
|
|
453
|
+
await client.flush();
|
|
454
|
+
const body = client.getLastRequestBody();
|
|
455
|
+
const score = body.batch.find((e) => e.type === 'score-create');
|
|
456
|
+
expect(score.body.observationId).toBe(gen.id);
|
|
457
|
+
expect(score.body.traceId).toBe(trace.id);
|
|
458
|
+
expect(score.body.name).toBe('accuracy');
|
|
459
|
+
expect(score.body.value).toBe(1.0);
|
|
460
|
+
expect(score.body.comment).toBe('Correct answer');
|
|
461
|
+
});
|
|
462
|
+
it('should support end() which sets endTime and merges params', async () => {
|
|
463
|
+
const client = createClient({ flushAt: 100 });
|
|
464
|
+
const trace = client.trace({ name: 'test' });
|
|
465
|
+
const gen = trace.generation({ model: 'claude-3-sonnet' });
|
|
466
|
+
gen.end({
|
|
467
|
+
output: 'done',
|
|
468
|
+
usage: { input: 100, output: 200 },
|
|
469
|
+
});
|
|
470
|
+
await client.flush();
|
|
471
|
+
const body = client.getLastRequestBody();
|
|
472
|
+
const genUpdate = body.batch.find((e) => e.type === 'generation-update');
|
|
473
|
+
expect(genUpdate.body.endTime).toBeDefined();
|
|
474
|
+
expect(genUpdate.body.output).toBe('done');
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
// -------------------------------------------------------------------------
|
|
478
|
+
// Constructor validation
|
|
479
|
+
// -------------------------------------------------------------------------
|
|
480
|
+
describe('constructor', () => {
|
|
481
|
+
it('should throw when publicKey is missing', () => {
|
|
482
|
+
expect(() => new TestClient({
|
|
483
|
+
publicKey: '',
|
|
484
|
+
secretKey: 'sk-test',
|
|
485
|
+
})).toThrow('publicKey and secretKey are required');
|
|
486
|
+
});
|
|
487
|
+
it('should throw when secretKey is missing', () => {
|
|
488
|
+
expect(() => new TestClient({
|
|
489
|
+
publicKey: 'pk-test',
|
|
490
|
+
secretKey: '',
|
|
491
|
+
})).toThrow('publicKey and secretKey are required');
|
|
492
|
+
});
|
|
493
|
+
it('should strip trailing slashes from baseUrl', async () => {
|
|
494
|
+
const client = createClient({
|
|
495
|
+
baseUrl: 'https://observe.test///',
|
|
496
|
+
});
|
|
497
|
+
client.enqueue({
|
|
498
|
+
id: 'evt-1',
|
|
499
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
500
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
501
|
+
body: { id: 'trace-1' },
|
|
502
|
+
});
|
|
503
|
+
await client.flush();
|
|
504
|
+
expect(client.fetchCalls[0].url).toBe('https://observe.test/api/public/ingestion');
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
// -------------------------------------------------------------------------
|
|
508
|
+
// Constructor config defaults
|
|
509
|
+
// -------------------------------------------------------------------------
|
|
510
|
+
describe('config defaults', () => {
|
|
511
|
+
it('should use default baseUrl when not provided', async () => {
|
|
512
|
+
const client = new TestClient({
|
|
513
|
+
publicKey: 'pk-test',
|
|
514
|
+
secretKey: 'sk-test',
|
|
515
|
+
flushInterval: 0,
|
|
516
|
+
});
|
|
517
|
+
client.enqueue({
|
|
518
|
+
id: 'evt-1',
|
|
519
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
520
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
521
|
+
body: { id: 'trace-1' },
|
|
522
|
+
});
|
|
523
|
+
await client.flush();
|
|
524
|
+
expect(client.fetchCalls[0].url).toBe('http://localhost:3000/api/public/ingestion');
|
|
525
|
+
});
|
|
526
|
+
it('should set all config fields correctly', () => {
|
|
527
|
+
// The client doesn't expose config fields publicly, so we verify
|
|
528
|
+
// behavior that depends on them
|
|
529
|
+
const client = createClient({
|
|
530
|
+
publicKey: 'pk-custom',
|
|
531
|
+
secretKey: 'sk-custom',
|
|
532
|
+
baseUrl: 'https://custom.example.com',
|
|
533
|
+
flushAt: 2,
|
|
534
|
+
enabled: true,
|
|
535
|
+
});
|
|
536
|
+
// flushAt = 2 — enqueue 2 events should trigger auto-flush
|
|
537
|
+
client.enqueue({
|
|
538
|
+
id: 'evt-1',
|
|
539
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
540
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
541
|
+
body: { id: 'trace-1' },
|
|
542
|
+
});
|
|
543
|
+
client.enqueue({
|
|
544
|
+
id: 'evt-2',
|
|
545
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
546
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
547
|
+
body: { id: 'trace-2' },
|
|
548
|
+
});
|
|
549
|
+
// Auto-flush was triggered (async), verify URL has custom base
|
|
550
|
+
// The fetch is async, so just verify the queue was drained
|
|
551
|
+
// We can verify the URL after advancing timers
|
|
552
|
+
return vi.advanceTimersByTimeAsync(0).then(() => {
|
|
553
|
+
expect(client.fetchCalls[0].url).toBe('https://custom.example.com/api/public/ingestion');
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
// -------------------------------------------------------------------------
|
|
558
|
+
// getQueue() behavior
|
|
559
|
+
// -------------------------------------------------------------------------
|
|
560
|
+
describe('getQueue()', () => {
|
|
561
|
+
it('should return the current events in the queue', () => {
|
|
562
|
+
const client = createClient();
|
|
563
|
+
client.enqueue({
|
|
564
|
+
id: 'evt-1',
|
|
565
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
566
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
567
|
+
body: { id: 'trace-1' },
|
|
568
|
+
});
|
|
569
|
+
const queue = client.getQueue();
|
|
570
|
+
expect(queue).toHaveLength(1);
|
|
571
|
+
expect(queue[0].id).toBe('evt-1');
|
|
572
|
+
});
|
|
573
|
+
it('should return a read-only array', () => {
|
|
574
|
+
const client = createClient();
|
|
575
|
+
const queue = client.getQueue();
|
|
576
|
+
// ReadonlyArray does not have push at runtime, but the reference
|
|
577
|
+
// should reflect the internal array state
|
|
578
|
+
expect(Array.isArray(queue)).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
// -------------------------------------------------------------------------
|
|
582
|
+
// trace() with config environment/release/version
|
|
583
|
+
// -------------------------------------------------------------------------
|
|
584
|
+
describe('trace() config propagation', () => {
|
|
585
|
+
it('should include environment from config when not in params', async () => {
|
|
586
|
+
const client = createClient({ environment: 'production', flushAt: 100 });
|
|
587
|
+
client.trace({ name: 'test-trace' });
|
|
588
|
+
await client.flush();
|
|
589
|
+
const body = client.getLastRequestBody();
|
|
590
|
+
const traceEvt = body.batch.find((e) => e.type === 'trace-create');
|
|
591
|
+
expect(traceEvt.body.environment).toBe('production');
|
|
592
|
+
});
|
|
593
|
+
it('should include release from config when not in params', async () => {
|
|
594
|
+
const client = createClient({ release: 'v1.2.3', flushAt: 100 });
|
|
595
|
+
client.trace({ name: 'test-trace' });
|
|
596
|
+
await client.flush();
|
|
597
|
+
const body = client.getLastRequestBody();
|
|
598
|
+
const traceEvt = body.batch.find((e) => e.type === 'trace-create');
|
|
599
|
+
expect(traceEvt.body.release).toBe('v1.2.3');
|
|
600
|
+
});
|
|
601
|
+
it('should include version from release config when not in params', async () => {
|
|
602
|
+
const client = createClient({ release: 'v2.0.0', flushAt: 100 });
|
|
603
|
+
client.trace({ name: 'test-trace' });
|
|
604
|
+
await client.flush();
|
|
605
|
+
const body = client.getLastRequestBody();
|
|
606
|
+
const traceEvt = body.batch.find((e) => e.type === 'trace-create');
|
|
607
|
+
expect(traceEvt.body.version).toBe('v2.0.0');
|
|
608
|
+
});
|
|
609
|
+
it('should not override params.environment with config.environment', async () => {
|
|
610
|
+
const client = createClient({
|
|
611
|
+
environment: 'production',
|
|
612
|
+
flushAt: 100,
|
|
613
|
+
});
|
|
614
|
+
client.trace({ name: 'test', environment: 'staging' });
|
|
615
|
+
await client.flush();
|
|
616
|
+
const body = client.getLastRequestBody();
|
|
617
|
+
const traceEvt = body.batch.find((e) => e.type === 'trace-create');
|
|
618
|
+
expect(traceEvt.body.environment).toBe('staging');
|
|
619
|
+
});
|
|
620
|
+
it('should not override params.release with config.release', async () => {
|
|
621
|
+
const client = createClient({ release: 'v1.0.0', flushAt: 100 });
|
|
622
|
+
client.trace({ name: 'test', release: 'v2.0.0' });
|
|
623
|
+
await client.flush();
|
|
624
|
+
const body = client.getLastRequestBody();
|
|
625
|
+
const traceEvt = body.batch.find((e) => e.type === 'trace-create');
|
|
626
|
+
expect(traceEvt.body.release).toBe('v2.0.0');
|
|
627
|
+
});
|
|
628
|
+
it('should not override params.version with config.release', async () => {
|
|
629
|
+
const client = createClient({ release: 'v1.0.0', flushAt: 100 });
|
|
630
|
+
client.trace({ name: 'test', version: 'custom-version' });
|
|
631
|
+
await client.flush();
|
|
632
|
+
const body = client.getLastRequestBody();
|
|
633
|
+
const traceEvt = body.batch.find((e) => e.type === 'trace-create');
|
|
634
|
+
expect(traceEvt.body.version).toBe('custom-version');
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
// -------------------------------------------------------------------------
|
|
638
|
+
// 207 Multi-Status handling
|
|
639
|
+
// -------------------------------------------------------------------------
|
|
640
|
+
describe('207 Multi-Status response', () => {
|
|
641
|
+
it('should handle 207 response with errors by logging and accepting', async () => {
|
|
642
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
643
|
+
const client = createClient({ flushAt: 100 });
|
|
644
|
+
client.mockResponse = {
|
|
645
|
+
status: 207,
|
|
646
|
+
statusText: 'Multi-Status',
|
|
647
|
+
ok: true,
|
|
648
|
+
json: async () => ({
|
|
649
|
+
successes: [{ id: 'evt-1', status: 200 }],
|
|
650
|
+
errors: [{ id: 'evt-2', status: 400, message: 'Invalid event' }],
|
|
651
|
+
}),
|
|
652
|
+
text: async () => '',
|
|
653
|
+
};
|
|
654
|
+
client.enqueue({
|
|
655
|
+
id: 'evt-1',
|
|
656
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
657
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
658
|
+
body: { id: 'trace-1' },
|
|
659
|
+
});
|
|
660
|
+
client.enqueue({
|
|
661
|
+
id: 'evt-2',
|
|
662
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
663
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
664
|
+
body: { id: 'trace-2' },
|
|
665
|
+
});
|
|
666
|
+
await client.flush();
|
|
667
|
+
// Batch accepted — queue should be empty
|
|
668
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
669
|
+
consoleSpy.mockRestore();
|
|
670
|
+
});
|
|
671
|
+
it('should handle 207 response with no errors', async () => {
|
|
672
|
+
const client = createClient({ flushAt: 100 });
|
|
673
|
+
client.mockResponse = {
|
|
674
|
+
status: 207,
|
|
675
|
+
statusText: 'Multi-Status',
|
|
676
|
+
ok: true,
|
|
677
|
+
json: async () => ({
|
|
678
|
+
successes: [{ id: 'evt-1', status: 200 }],
|
|
679
|
+
errors: [],
|
|
680
|
+
}),
|
|
681
|
+
text: async () => '',
|
|
682
|
+
};
|
|
683
|
+
client.enqueue({
|
|
684
|
+
id: 'evt-1',
|
|
685
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
686
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
687
|
+
body: { id: 'trace-1' },
|
|
688
|
+
});
|
|
689
|
+
await client.flush();
|
|
690
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
// -------------------------------------------------------------------------
|
|
694
|
+
// Flush clears queue after successful send
|
|
695
|
+
// -------------------------------------------------------------------------
|
|
696
|
+
describe('flush queue clearing', () => {
|
|
697
|
+
it('should clear queue after successful send', async () => {
|
|
698
|
+
const client = createClient({ flushAt: 100 });
|
|
699
|
+
client.enqueue({
|
|
700
|
+
id: 'evt-1',
|
|
701
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
702
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
703
|
+
body: { id: 'trace-1' },
|
|
704
|
+
});
|
|
705
|
+
client.enqueue({
|
|
706
|
+
id: 'evt-2',
|
|
707
|
+
type: IngestionEventType.SPAN_CREATE,
|
|
708
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
709
|
+
body: { id: 'span-1' },
|
|
710
|
+
});
|
|
711
|
+
expect(client.getQueue()).toHaveLength(2);
|
|
712
|
+
await client.flush();
|
|
713
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
// -------------------------------------------------------------------------
|
|
717
|
+
// Concurrent flush guard
|
|
718
|
+
// -------------------------------------------------------------------------
|
|
719
|
+
describe('concurrent flush guard', () => {
|
|
720
|
+
it('should skip flush if one is already in progress', async () => {
|
|
721
|
+
const client = createClient({ flushAt: 100 });
|
|
722
|
+
// Make fetchWithRetry slow
|
|
723
|
+
let resolveFirst;
|
|
724
|
+
const originalMockResponse = client.mockResponse;
|
|
725
|
+
let callCount = 0;
|
|
726
|
+
// Override to add delay on first call
|
|
727
|
+
const origFetchCalls = client.fetchCalls;
|
|
728
|
+
client.enqueue({
|
|
729
|
+
id: 'evt-1',
|
|
730
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
731
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
732
|
+
body: { id: 'trace-1' },
|
|
733
|
+
});
|
|
734
|
+
// Start first flush (won't resolve immediately)
|
|
735
|
+
const firstFlush = client.flush();
|
|
736
|
+
// Enqueue more and try second flush while first is in progress
|
|
737
|
+
client.enqueue({
|
|
738
|
+
id: 'evt-2',
|
|
739
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
740
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
741
|
+
body: { id: 'trace-2' },
|
|
742
|
+
});
|
|
743
|
+
// This should be a no-op since flush is already in progress
|
|
744
|
+
const secondFlush = client.flush();
|
|
745
|
+
await firstFlush;
|
|
746
|
+
await secondFlush;
|
|
747
|
+
// First flush sent 1 event, second was skipped
|
|
748
|
+
// evt-2 should still be in the queue
|
|
749
|
+
expect(client.getQueue()).toHaveLength(1);
|
|
750
|
+
expect(client.getQueue()[0].id).toBe('evt-2');
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
// -------------------------------------------------------------------------
|
|
754
|
+
// 4xx error handling (drop batch)
|
|
755
|
+
// -------------------------------------------------------------------------
|
|
756
|
+
describe('4xx error handling', () => {
|
|
757
|
+
it('should drop batch on 4xx client errors (not 429)', async () => {
|
|
758
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
759
|
+
const client = createClient({ flushAt: 100, maxRetries: 0 });
|
|
760
|
+
client.mockResponse = {
|
|
761
|
+
status: 401,
|
|
762
|
+
statusText: 'Unauthorized',
|
|
763
|
+
ok: false,
|
|
764
|
+
json: async () => ({}),
|
|
765
|
+
text: async () => 'Invalid API key',
|
|
766
|
+
};
|
|
767
|
+
client.enqueue({
|
|
768
|
+
id: 'evt-1',
|
|
769
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
770
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
771
|
+
body: { id: 'trace-1' },
|
|
772
|
+
});
|
|
773
|
+
await client.flush();
|
|
774
|
+
// 4xx errors drop the batch, not re-queue
|
|
775
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
776
|
+
consoleSpy.mockRestore();
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
// -------------------------------------------------------------------------
|
|
780
|
+
// Disabled client: score and flush
|
|
781
|
+
// -------------------------------------------------------------------------
|
|
782
|
+
describe('disabled client edge cases', () => {
|
|
783
|
+
it('should not enqueue scores when disabled', () => {
|
|
784
|
+
const client = createClient({ enabled: false });
|
|
785
|
+
client.score({ traceId: 'trace-1', name: 'test', value: 1 });
|
|
786
|
+
expect(client.getQueue()).toHaveLength(0);
|
|
787
|
+
});
|
|
788
|
+
it('should not send on flush when disabled', async () => {
|
|
789
|
+
const client = createClient({ enabled: false });
|
|
790
|
+
await client.flush();
|
|
791
|
+
expect(client.fetchCalls).toHaveLength(0);
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
// -------------------------------------------------------------------------
|
|
795
|
+
// Flush error handling
|
|
796
|
+
// -------------------------------------------------------------------------
|
|
797
|
+
describe('flush error handling', () => {
|
|
798
|
+
it('should re-queue events when fetch throws an error', async () => {
|
|
799
|
+
const client = createClient({ flushAt: 100, maxRetries: 0 });
|
|
800
|
+
// Make fetch throw
|
|
801
|
+
client.mockResponse = {
|
|
802
|
+
status: 500,
|
|
803
|
+
statusText: 'Internal Server Error',
|
|
804
|
+
ok: false,
|
|
805
|
+
json: async () => ({}),
|
|
806
|
+
text: async () => 'server error',
|
|
807
|
+
};
|
|
808
|
+
// Suppress console.error from the logger
|
|
809
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
810
|
+
client.enqueue({
|
|
811
|
+
id: 'evt-1',
|
|
812
|
+
type: IngestionEventType.TRACE_CREATE,
|
|
813
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
814
|
+
body: { id: 'trace-1' },
|
|
815
|
+
});
|
|
816
|
+
await client.flush();
|
|
817
|
+
// Events should be re-queued after failure
|
|
818
|
+
expect(client.getQueue()).toHaveLength(1);
|
|
819
|
+
expect(client.getQueue()[0].id).toBe('evt-1');
|
|
820
|
+
consoleSpy.mockRestore();
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
// -------------------------------------------------------------------------
|
|
824
|
+
// Standalone score
|
|
825
|
+
// -------------------------------------------------------------------------
|
|
826
|
+
describe('standalone score', () => {
|
|
827
|
+
it('should enqueue a score event via client.score()', async () => {
|
|
828
|
+
const client = createClient({ flushAt: 100 });
|
|
829
|
+
client.score({
|
|
830
|
+
traceId: 'trace-external',
|
|
831
|
+
name: 'user-feedback',
|
|
832
|
+
value: 5,
|
|
833
|
+
comment: 'Great response',
|
|
834
|
+
});
|
|
835
|
+
await client.flush();
|
|
836
|
+
const body = client.getLastRequestBody();
|
|
837
|
+
const score = body.batch.find((e) => e.type === 'score-create');
|
|
838
|
+
expect(score.body.traceId).toBe('trace-external');
|
|
839
|
+
expect(score.body.name).toBe('user-feedback');
|
|
840
|
+
expect(score.body.value).toBe(5);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
//# sourceMappingURL=core-client.test.js.map
|