@comprehend/telemetry-node 0.1.3 → 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.
Files changed (42) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.idea/telemetry-node.iml +0 -1
  3. package/DEVELOPMENT.md +69 -0
  4. package/README.md +173 -0
  5. package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
  6. package/dist/ComprehendDevSpanProcessor.js +146 -87
  7. package/dist/ComprehendDevSpanProcessor.test.d.ts +1 -0
  8. package/dist/ComprehendDevSpanProcessor.test.js +495 -0
  9. package/dist/ComprehendMetricsExporter.d.ts +18 -0
  10. package/dist/ComprehendMetricsExporter.js +178 -0
  11. package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
  12. package/dist/ComprehendMetricsExporter.test.js +266 -0
  13. package/dist/ComprehendSDK.d.ts +18 -0
  14. package/dist/ComprehendSDK.js +56 -0
  15. package/dist/ComprehendSDK.test.d.ts +1 -0
  16. package/dist/ComprehendSDK.test.js +126 -0
  17. package/dist/WebSocketConnection.d.ts +23 -3
  18. package/dist/WebSocketConnection.js +106 -12
  19. package/dist/WebSocketConnection.test.d.ts +1 -0
  20. package/dist/WebSocketConnection.test.js +473 -0
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +5 -1
  23. package/dist/sql-analyzer.js +2 -11
  24. package/dist/sql-analyzer.test.js +0 -12
  25. package/dist/util.d.ts +2 -0
  26. package/dist/util.js +7 -0
  27. package/dist/wire-protocol.d.ts +168 -28
  28. package/jest.config.js +1 -0
  29. package/package.json +4 -2
  30. package/src/ComprehendDevSpanProcessor.test.ts +626 -0
  31. package/src/ComprehendDevSpanProcessor.ts +170 -105
  32. package/src/ComprehendMetricsExporter.test.ts +334 -0
  33. package/src/ComprehendMetricsExporter.ts +225 -0
  34. package/src/ComprehendSDK.test.ts +160 -0
  35. package/src/ComprehendSDK.ts +63 -0
  36. package/src/WebSocketConnection.test.ts +616 -0
  37. package/src/WebSocketConnection.ts +135 -13
  38. package/src/index.ts +3 -2
  39. package/src/util.ts +6 -0
  40. package/src/wire-protocol.ts +204 -29
  41. package/src/sql-analyzer.test.ts +0 -599
  42. package/src/sql-analyzer.ts +0 -439
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const WebSocketConnection_1 = require("./WebSocketConnection");
7
+ // Mock the ws WebSocket library
8
+ jest.mock('ws');
9
+ // Mock crypto.randomUUID
10
+ jest.mock('crypto', () => ({
11
+ randomUUID: jest.fn().mockReturnValue('test-uuid-1234'),
12
+ }));
13
+ const ws_1 = __importDefault(require("ws"));
14
+ const MockedWebSocket = ws_1.default;
15
+ function authAck(customMetrics = []) {
16
+ return JSON.stringify({ type: 'ack-authorized', customMetrics });
17
+ }
18
+ describe('WebSocketConnection', () => {
19
+ let mockSocket;
20
+ let connection;
21
+ let logMessages;
22
+ let mockLogger;
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ jest.clearAllTimers();
26
+ jest.useFakeTimers();
27
+ logMessages = [];
28
+ mockLogger = jest.fn((message) => {
29
+ logMessages.push(message);
30
+ });
31
+ mockSocket = {
32
+ on: jest.fn(),
33
+ send: jest.fn(),
34
+ close: jest.fn(),
35
+ readyState: ws_1.default.CLOSED,
36
+ OPEN: ws_1.default.OPEN,
37
+ CLOSED: ws_1.default.CLOSED,
38
+ _triggerOpen: function () { },
39
+ _triggerMessage: function (data) { },
40
+ _triggerClose: function (code, reason) { },
41
+ _triggerError: function (error) { }
42
+ };
43
+ const eventHandlers = {};
44
+ mockSocket.on.mockImplementation((event, handler) => {
45
+ eventHandlers[event] = handler;
46
+ });
47
+ mockSocket._triggerOpen = () => {
48
+ mockSocket.readyState = ws_1.default.OPEN;
49
+ eventHandlers['open']?.();
50
+ };
51
+ mockSocket._triggerMessage = (data) => {
52
+ const buffer = Buffer.from(data);
53
+ eventHandlers['message']?.(buffer);
54
+ };
55
+ mockSocket._triggerClose = (code, reason) => {
56
+ mockSocket.readyState = ws_1.default.CLOSED;
57
+ eventHandlers['close']?.(code, Buffer.from(reason));
58
+ };
59
+ mockSocket._triggerError = (error) => {
60
+ eventHandlers['error']?.(error);
61
+ };
62
+ MockedWebSocket.mockImplementation(() => mockSocket);
63
+ });
64
+ afterEach(() => {
65
+ jest.useRealTimers();
66
+ if (connection) {
67
+ connection.close();
68
+ }
69
+ });
70
+ describe('Connection Establishment', () => {
71
+ it('should create WebSocket connection with correct URL', () => {
72
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
73
+ expect(MockedWebSocket).toHaveBeenCalledWith('wss://ingestion.comprehend.dev/test-org/observations');
74
+ expect(mockSocket.on).toHaveBeenCalledWith('open', expect.any(Function));
75
+ expect(mockSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
76
+ expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
77
+ expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
78
+ });
79
+ it('should send V2 init message on connection open', () => {
80
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
81
+ mockSocket._triggerOpen();
82
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify({
83
+ event: 'init',
84
+ protocolVersion: 2,
85
+ token: 'test-token'
86
+ }));
87
+ expect(logMessages).toContain('WebSocket connected. Sending init/auth message.');
88
+ });
89
+ it('should work without logger', () => {
90
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token' });
91
+ mockSocket._triggerOpen();
92
+ expect(MockedWebSocket).toHaveBeenCalled();
93
+ expect(mockSocket.send).toHaveBeenCalled();
94
+ });
95
+ });
96
+ describe('Authorization Flow', () => {
97
+ beforeEach(() => {
98
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
99
+ mockSocket._triggerOpen();
100
+ jest.clearAllMocks();
101
+ });
102
+ it('should handle authorization acknowledgment with customMetrics', () => {
103
+ mockSocket._triggerMessage(authAck([{ type: 'cumulative', id: 'test', attributes: [], subject: 'sub' }]));
104
+ expect(logMessages).toContain('Authorization acknowledged by server.');
105
+ });
106
+ it('should call onAuthorized callback with InitAck', () => {
107
+ const onAuthorized = jest.fn();
108
+ connection = new WebSocketConnection_1.WebSocketConnection({
109
+ organization: 'test-org', token: 'test-token', logger: mockLogger, onAuthorized
110
+ });
111
+ mockSocket._triggerOpen();
112
+ const customMetrics = [{ type: 'cumulative', id: 'test', attributes: [], subject: 'sub' }];
113
+ mockSocket._triggerMessage(authAck(customMetrics));
114
+ expect(onAuthorized).toHaveBeenCalledWith({ type: 'ack-authorized', customMetrics });
115
+ });
116
+ it('should replay queued messages after authorization', () => {
117
+ const serviceMessage = {
118
+ event: 'new-entity',
119
+ type: 'service',
120
+ hash: 'test-hash-1',
121
+ name: 'test-service'
122
+ };
123
+ const observationMessage = {
124
+ event: 'observations',
125
+ seq: 1,
126
+ observations: [{
127
+ type: 'http-server',
128
+ subject: 'test-subject',
129
+ spanId: 'span1',
130
+ traceId: 'trace1',
131
+ timestamp: [1700000000, 0],
132
+ path: '/test',
133
+ status: 200,
134
+ duration: [0, 100000000]
135
+ }]
136
+ };
137
+ connection.sendMessage(serviceMessage);
138
+ connection.sendMessage(observationMessage);
139
+ expect(mockSocket.send).not.toHaveBeenCalled();
140
+ mockSocket._triggerMessage(authAck());
141
+ expect(mockSocket.send).toHaveBeenCalledTimes(2);
142
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
143
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
144
+ });
145
+ it('should send messages immediately when already authorized', () => {
146
+ mockSocket._triggerMessage(authAck());
147
+ jest.clearAllMocks();
148
+ const serviceMessage = {
149
+ event: 'new-entity',
150
+ type: 'service',
151
+ hash: 'test-hash',
152
+ name: 'test-service'
153
+ };
154
+ connection.sendMessage(serviceMessage);
155
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
156
+ });
157
+ });
158
+ describe('Context Handling', () => {
159
+ beforeEach(() => {
160
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
161
+ mockSocket._triggerOpen();
162
+ });
163
+ it('should send context-start after auth when process context is set before auth', () => {
164
+ connection.setProcessContext('service-hash', { 'service.name': 'test' });
165
+ // Not authorized yet, so no context-start sent
166
+ mockSocket.send.mockClear();
167
+ mockSocket._triggerMessage(authAck());
168
+ // Should have sent context-start first
169
+ const sentMessages = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]));
170
+ const contextMsg = sentMessages.find((m) => m.event === 'context-start');
171
+ expect(contextMsg).toMatchObject({
172
+ event: 'context-start',
173
+ type: 'process',
174
+ serviceEntityHash: 'service-hash',
175
+ resources: { 'service.name': 'test' },
176
+ ingestionId: expect.any(String),
177
+ });
178
+ });
179
+ it('should send context-start immediately when already authorized', () => {
180
+ mockSocket._triggerMessage(authAck());
181
+ mockSocket.send.mockClear();
182
+ connection.setProcessContext('service-hash', { 'service.name': 'test' });
183
+ const sentMessages = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]));
184
+ expect(sentMessages[0]).toMatchObject({
185
+ event: 'context-start',
186
+ type: 'process',
187
+ serviceEntityHash: 'service-hash',
188
+ });
189
+ });
190
+ it('should re-send context-start on reconnect before other replays', () => {
191
+ connection.setProcessContext('service-hash', { 'service.name': 'test' });
192
+ mockSocket._triggerMessage(authAck());
193
+ // Send an entity message
194
+ const entityMsg = {
195
+ event: 'new-entity', type: 'service', hash: 'h1', name: 'svc'
196
+ };
197
+ connection.sendMessage(entityMsg);
198
+ // Reconnect
199
+ mockSocket._triggerClose(1000, 'test');
200
+ jest.advanceTimersByTime(1000);
201
+ mockSocket._triggerOpen();
202
+ mockSocket.send.mockClear();
203
+ mockSocket._triggerMessage(authAck());
204
+ const sentMessages = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]));
205
+ // Context should come first
206
+ expect(sentMessages[0].event).toBe('context-start');
207
+ });
208
+ });
209
+ describe('Message Acknowledgment Handling', () => {
210
+ beforeEach(() => {
211
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
212
+ mockSocket._triggerOpen();
213
+ mockSocket._triggerMessage(authAck());
214
+ jest.clearAllMocks();
215
+ });
216
+ it('should handle entity/interaction acknowledgments', () => {
217
+ const serviceMessage = {
218
+ event: 'new-entity', type: 'service', hash: 'service-hash', name: 'test-service'
219
+ };
220
+ const routeMessage = {
221
+ event: 'new-entity', type: 'http-route', hash: 'route-hash',
222
+ parent: 'service-hash', method: 'GET', route: '/test'
223
+ };
224
+ connection.sendMessage(serviceMessage);
225
+ connection.sendMessage(routeMessage);
226
+ // Acknowledge the service
227
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-observed', hash: 'service-hash' }));
228
+ mockSocket.send.mockClear();
229
+ // Reconnect — only unacked route should replay
230
+ mockSocket._triggerClose(1000, 'test close');
231
+ jest.advanceTimersByTime(1000);
232
+ mockSocket._triggerOpen();
233
+ mockSocket.send.mockClear();
234
+ mockSocket._triggerMessage(authAck());
235
+ expect(mockSocket.send).toHaveBeenCalledTimes(1);
236
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(routeMessage));
237
+ });
238
+ it('should handle observation acknowledgments', () => {
239
+ const obs1 = { event: 'observations', seq: 1, observations: [] };
240
+ const obs2 = { event: 'observations', seq: 2, observations: [] };
241
+ connection.sendMessage(obs1);
242
+ connection.sendMessage(obs2);
243
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-observations', seq: 1 }));
244
+ mockSocket.send.mockClear();
245
+ mockSocket._triggerClose(1000, 'test');
246
+ jest.advanceTimersByTime(1000);
247
+ mockSocket._triggerOpen();
248
+ mockSocket.send.mockClear();
249
+ mockSocket._triggerMessage(authAck());
250
+ expect(mockSocket.send).toHaveBeenCalledTimes(1);
251
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(obs2));
252
+ });
253
+ it('should handle timeseries acknowledgments', () => {
254
+ const msg = {
255
+ event: 'timeseries', seq: 5, data: []
256
+ };
257
+ connection.sendMessage(msg);
258
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-timeseries', seq: 5 }));
259
+ mockSocket.send.mockClear();
260
+ mockSocket._triggerClose(1000, 'test');
261
+ jest.advanceTimersByTime(1000);
262
+ mockSocket._triggerOpen();
263
+ mockSocket.send.mockClear();
264
+ mockSocket._triggerMessage(authAck());
265
+ // Should not replay the acked message
266
+ const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
267
+ expect(sentEvents).not.toContain('timeseries');
268
+ });
269
+ it('should handle cumulative acknowledgments', () => {
270
+ const msg = { event: 'cumulative', seq: 3, data: [] };
271
+ connection.sendMessage(msg);
272
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-cumulative', seq: 3 }));
273
+ mockSocket.send.mockClear();
274
+ mockSocket._triggerClose(1000, 'test');
275
+ jest.advanceTimersByTime(1000);
276
+ mockSocket._triggerOpen();
277
+ mockSocket.send.mockClear();
278
+ mockSocket._triggerMessage(authAck());
279
+ const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
280
+ expect(sentEvents).not.toContain('cumulative');
281
+ });
282
+ it('should handle tracespans acknowledgments', () => {
283
+ const msg = { event: 'tracespans', seq: 7, data: [] };
284
+ connection.sendMessage(msg);
285
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-tracespans', seq: 7 }));
286
+ mockSocket.send.mockClear();
287
+ mockSocket._triggerClose(1000, 'test');
288
+ jest.advanceTimersByTime(1000);
289
+ mockSocket._triggerOpen();
290
+ mockSocket.send.mockClear();
291
+ mockSocket._triggerMessage(authAck());
292
+ const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
293
+ expect(sentEvents).not.toContain('tracespans');
294
+ });
295
+ it('should handle db-query acknowledgments', () => {
296
+ const msg = {
297
+ event: 'db-query', seq: 9, query: 'SELECT 1', from: 'a', to: 'b',
298
+ timestamp: [0, 0], duration: [0, 0]
299
+ };
300
+ connection.sendMessage(msg);
301
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-db-query', seq: 9 }));
302
+ mockSocket.send.mockClear();
303
+ mockSocket._triggerClose(1000, 'test');
304
+ jest.advanceTimersByTime(1000);
305
+ mockSocket._triggerOpen();
306
+ mockSocket.send.mockClear();
307
+ mockSocket._triggerMessage(authAck());
308
+ const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
309
+ expect(sentEvents).not.toContain('db-query');
310
+ });
311
+ it('should handle context acknowledgments', () => {
312
+ connection.setProcessContext('svc-hash', { 'service.name': 'test' });
313
+ mockSocket._triggerMessage(authAck());
314
+ // Find the context seq
315
+ const contextCall = mockSocket.send.mock.calls.find(c => {
316
+ const m = JSON.parse(c[0]);
317
+ return m.event === 'context-start';
318
+ });
319
+ const contextSeq = JSON.parse(contextCall[0]).seq;
320
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-context', seq: contextSeq }));
321
+ // On reconnect, context-start should still be sent (because setProcessContext data persists)
322
+ // but the acked one should be removed from the unack queue
323
+ });
324
+ });
325
+ describe('Custom Metric Change', () => {
326
+ it('should call onCustomMetricChange callback', () => {
327
+ const onCustomMetricChange = jest.fn();
328
+ connection = new WebSocketConnection_1.WebSocketConnection({
329
+ organization: 'test-org', token: 'test-token', onCustomMetricChange
330
+ });
331
+ mockSocket._triggerOpen();
332
+ mockSocket._triggerMessage(authAck());
333
+ const specs = [
334
+ { type: 'timeseries', id: 'metric1', attributes: ['a'], subject: 'sub1' }
335
+ ];
336
+ mockSocket._triggerMessage(JSON.stringify({
337
+ type: 'custom-metric-change',
338
+ customMetrics: specs
339
+ }));
340
+ expect(onCustomMetricChange).toHaveBeenCalledWith(specs);
341
+ });
342
+ });
343
+ describe('Shared Sequence Counter', () => {
344
+ it('should provide incrementing sequence numbers', () => {
345
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token' });
346
+ expect(connection.nextSeq()).toBe(1);
347
+ expect(connection.nextSeq()).toBe(2);
348
+ expect(connection.nextSeq()).toBe(3);
349
+ });
350
+ });
351
+ describe('Reconnection Logic', () => {
352
+ beforeEach(() => {
353
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
354
+ mockSocket._triggerOpen();
355
+ });
356
+ it('should reconnect automatically on close', () => {
357
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
358
+ mockSocket._triggerClose(1000, 'Normal closure');
359
+ expect(logMessages).toContain('WebSocket disconnected. Code: 1000, Reason: Normal closure');
360
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
361
+ jest.advanceTimersByTime(1000);
362
+ expect(MockedWebSocket).toHaveBeenCalledTimes(2);
363
+ setTimeoutSpy.mockRestore();
364
+ });
365
+ it('should reset authorization state on close', () => {
366
+ mockSocket._triggerMessage(authAck());
367
+ const testMessage = {
368
+ event: 'new-entity', type: 'service', hash: 'test-hash', name: 'test-service'
369
+ };
370
+ connection.sendMessage(testMessage);
371
+ expect(mockSocket.send).toHaveBeenCalled();
372
+ jest.clearAllMocks();
373
+ mockSocket._triggerClose(1000, 'test');
374
+ connection.sendMessage(testMessage);
375
+ expect(mockSocket.send).not.toHaveBeenCalled();
376
+ });
377
+ it('should not reconnect when explicitly closed', () => {
378
+ connection.close();
379
+ mockSocket._triggerClose(1000, 'Explicit close');
380
+ jest.advanceTimersByTime(5000);
381
+ expect(MockedWebSocket).toHaveBeenCalledTimes(1);
382
+ });
383
+ });
384
+ describe('Error Handling', () => {
385
+ beforeEach(() => {
386
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
387
+ });
388
+ it('should log WebSocket errors', () => {
389
+ const testError = new Error('Connection failed');
390
+ mockSocket._triggerError(testError);
391
+ expect(logMessages).toContain('WebSocket encountered an error: Connection failed');
392
+ });
393
+ it('should handle malformed server messages gracefully', () => {
394
+ mockSocket._triggerOpen();
395
+ mockSocket._triggerMessage('{invalid json}');
396
+ const errorLogExists = logMessages.some(msg => msg.startsWith('Error parsing message from server:') &&
397
+ msg.includes('JSON'));
398
+ expect(errorLogExists).toBe(true);
399
+ });
400
+ it('should not send messages when socket is not open', () => {
401
+ mockSocket.readyState = ws_1.default.CLOSED;
402
+ mockSocket._triggerOpen();
403
+ mockSocket._triggerMessage(authAck());
404
+ mockSocket.send.mockClear();
405
+ mockSocket.readyState = ws_1.default.CLOSED;
406
+ const testMessage = {
407
+ event: 'new-entity', type: 'service', hash: 'test-hash', name: 'test-service'
408
+ };
409
+ connection.sendMessage(testMessage);
410
+ expect(mockSocket.send).not.toHaveBeenCalled();
411
+ });
412
+ });
413
+ describe('Connection Lifecycle', () => {
414
+ it('should close WebSocket connection properly', () => {
415
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
416
+ connection.close();
417
+ expect(mockSocket.close).toHaveBeenCalled();
418
+ });
419
+ it('should handle close when socket is null', () => {
420
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
421
+ connection.socket = null;
422
+ expect(() => connection.close()).not.toThrow();
423
+ });
424
+ });
425
+ describe('Message Queuing for All Types', () => {
426
+ beforeEach(() => {
427
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
428
+ mockSocket._triggerOpen();
429
+ mockSocket.send.mockClear();
430
+ });
431
+ it('should queue and replay timeseries messages', () => {
432
+ const msg = {
433
+ event: 'timeseries', seq: 1, data: [
434
+ { subject: 's', type: 't', timestamp: [0, 0], value: 42, unit: 'By', attributes: {} }
435
+ ]
436
+ };
437
+ connection.sendMessage(msg);
438
+ expect(mockSocket.send).not.toHaveBeenCalled();
439
+ mockSocket._triggerMessage(authAck());
440
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
441
+ });
442
+ it('should queue and replay cumulative messages', () => {
443
+ const msg = {
444
+ event: 'cumulative', seq: 2, data: []
445
+ };
446
+ connection.sendMessage(msg);
447
+ expect(mockSocket.send).not.toHaveBeenCalled();
448
+ mockSocket._triggerMessage(authAck());
449
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
450
+ });
451
+ it('should queue and replay tracespans messages', () => {
452
+ const msg = {
453
+ event: 'tracespans', seq: 3, data: [
454
+ { trace: 't1', span: 's1', parent: '', name: 'test', timestamp: [0, 0] }
455
+ ]
456
+ };
457
+ connection.sendMessage(msg);
458
+ expect(mockSocket.send).not.toHaveBeenCalled();
459
+ mockSocket._triggerMessage(authAck());
460
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
461
+ });
462
+ it('should queue and replay db-query messages', () => {
463
+ const msg = {
464
+ event: 'db-query', seq: 4, query: 'SELECT 1', from: 'a', to: 'b',
465
+ timestamp: [0, 0], duration: [0, 1000000]
466
+ };
467
+ connection.sendMessage(msg);
468
+ expect(mockSocket.send).not.toHaveBeenCalled();
469
+ mockSocket._triggerMessage(authAck());
470
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
471
+ });
472
+ });
473
+ });
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
- export { ComprehendDevSpanProcessor } from "./ComprehendDevSpanProcessor";
1
+ export { ComprehendSDK } from './ComprehendSDK';
2
+ export { ComprehendDevSpanProcessor } from './ComprehendDevSpanProcessor';
3
+ export { ComprehendMetricsExporter } from './ComprehendMetricsExporter';
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ComprehendDevSpanProcessor = void 0;
3
+ exports.ComprehendMetricsExporter = exports.ComprehendDevSpanProcessor = exports.ComprehendSDK = void 0;
4
+ var ComprehendSDK_1 = require("./ComprehendSDK");
5
+ Object.defineProperty(exports, "ComprehendSDK", { enumerable: true, get: function () { return ComprehendSDK_1.ComprehendSDK; } });
4
6
  var ComprehendDevSpanProcessor_1 = require("./ComprehendDevSpanProcessor");
5
7
  Object.defineProperty(exports, "ComprehendDevSpanProcessor", { enumerable: true, get: function () { return ComprehendDevSpanProcessor_1.ComprehendDevSpanProcessor; } });
8
+ var ComprehendMetricsExporter_1 = require("./ComprehendMetricsExporter");
9
+ Object.defineProperty(exports, "ComprehendMetricsExporter", { enumerable: true, get: function () { return ComprehendMetricsExporter_1.ComprehendMetricsExporter; } });
@@ -23,7 +23,6 @@ function analyzeSQL(sql) {
23
23
  let skippingValues = false;
24
24
  let lookingForCommaOrEnd = false;
25
25
  let valuesDepth = 0;
26
- let skippedWhitespace = [];
27
26
  for (let token of tokenizeSQL(sql)) {
28
27
  switch (token.type) {
29
28
  case "whitespace":
@@ -137,31 +136,23 @@ function analyzeSQL(sql) {
137
136
  switch (token.type) {
138
137
  case "comment":
139
138
  case "whitespace":
140
- // Collect whitespace/comments while looking for comma or end
141
- skippedWhitespace.push(token);
139
+ // Skip whitespace/comments while looking for comma or end
142
140
  break;
143
141
  case "punct":
144
142
  if (token.value === ",") {
145
- // More tuples coming, clear skipped whitespace and continue skipping
146
- skippedWhitespace = [];
143
+ // More tuples coming, continue skipping
147
144
  lookingForCommaOrEnd = false;
148
145
  skippingValues = true;
149
146
  }
150
147
  else {
151
148
  // Not a comma, so VALUES clause is done
152
- // Add back the skipped whitespace, then the current token
153
- presentableTokens.push(...skippedWhitespace);
154
149
  presentableTokens.push(token);
155
- skippedWhitespace = [];
156
150
  lookingForCommaOrEnd = false;
157
151
  }
158
152
  break;
159
153
  default:
160
154
  // VALUES clause is done, resume normal processing
161
- // Add back the skipped whitespace, then the current token
162
- presentableTokens.push(...skippedWhitespace);
163
155
  presentableTokens.push(token);
164
- skippedWhitespace = [];
165
156
  lookingForCommaOrEnd = false;
166
157
  break;
167
158
  }
@@ -482,16 +482,4 @@ describe('SQL Analyzer - bulk INSERT VALUES cardinality reduction', () => {
482
482
  expect(result.presentableQuery).toEqual(`INSERT INTO comments (text, author) VALUES
483
483
  (...)`);
484
484
  });
485
- it('preserves whitespace before ON CONFLICT after VALUES clause', () => {
486
- const sql = `INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com') ON CONFLICT (email) DO NOTHING`;
487
- const result = (0, sql_analyzer_1.analyzeSQL)(sql);
488
- expect(result.tableOperations).toEqual({ users: ['INSERT'] });
489
- expect(result.presentableQuery).toEqual(`INSERT INTO users (name, email) VALUES (...) ON CONFLICT (email) DO NOTHING`);
490
- });
491
- it('preserves whitespace before ON CONFLICT with multiple VALUES tuples', () => {
492
- const sql = `INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name`;
493
- const result = (0, sql_analyzer_1.analyzeSQL)(sql);
494
- expect(result.tableOperations).toEqual({ users: ['INSERT'] });
495
- expect(result.presentableQuery).toEqual(`INSERT INTO users (name, email) VALUES (...) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name`);
496
- });
497
485
  });
package/dist/util.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { HrTime } from './wire-protocol';
2
+ export declare function hrTimeNow(): HrTime;
package/dist/util.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hrTimeNow = hrTimeNow;
4
+ function hrTimeNow() {
5
+ const now = Date.now();
6
+ return [Math.floor(now / 1000), (now % 1000) * 1000000];
7
+ }