@comprehend/telemetry-node 0.1.4 → 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 (38) hide show
  1. package/.claude/settings.local.json +2 -2
  2. package/.idea/telemetry-node.iml +0 -1
  3. package/README.md +73 -27
  4. package/dist/ComprehendDevSpanProcessor.d.ts +9 -6
  5. package/dist/ComprehendDevSpanProcessor.js +145 -87
  6. package/dist/ComprehendDevSpanProcessor.test.js +270 -449
  7. package/dist/ComprehendMetricsExporter.d.ts +18 -0
  8. package/dist/ComprehendMetricsExporter.js +178 -0
  9. package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
  10. package/dist/ComprehendMetricsExporter.test.js +266 -0
  11. package/dist/ComprehendSDK.d.ts +18 -0
  12. package/dist/ComprehendSDK.js +56 -0
  13. package/dist/ComprehendSDK.test.d.ts +1 -0
  14. package/dist/ComprehendSDK.test.js +126 -0
  15. package/dist/WebSocketConnection.d.ts +23 -3
  16. package/dist/WebSocketConnection.js +106 -12
  17. package/dist/WebSocketConnection.test.js +236 -169
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +5 -1
  20. package/dist/sql-analyzer.js +2 -11
  21. package/dist/sql-analyzer.test.js +0 -12
  22. package/dist/util.d.ts +2 -0
  23. package/dist/util.js +7 -0
  24. package/dist/wire-protocol.d.ts +168 -28
  25. package/package.json +3 -1
  26. package/src/ComprehendDevSpanProcessor.test.ts +311 -507
  27. package/src/ComprehendDevSpanProcessor.ts +169 -105
  28. package/src/ComprehendMetricsExporter.test.ts +334 -0
  29. package/src/ComprehendMetricsExporter.ts +225 -0
  30. package/src/ComprehendSDK.test.ts +160 -0
  31. package/src/ComprehendSDK.ts +63 -0
  32. package/src/WebSocketConnection.test.ts +286 -205
  33. package/src/WebSocketConnection.ts +135 -13
  34. package/src/index.ts +3 -2
  35. package/src/util.ts +6 -0
  36. package/src/wire-protocol.ts +204 -29
  37. package/src/sql-analyzer.test.ts +0 -599
  38. package/src/sql-analyzer.ts +0 -439
@@ -6,15 +6,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const WebSocketConnection_1 = require("./WebSocketConnection");
7
7
  // Mock the ws WebSocket library
8
8
  jest.mock('ws');
9
+ // Mock crypto.randomUUID
10
+ jest.mock('crypto', () => ({
11
+ randomUUID: jest.fn().mockReturnValue('test-uuid-1234'),
12
+ }));
9
13
  const ws_1 = __importDefault(require("ws"));
10
14
  const MockedWebSocket = ws_1.default;
15
+ function authAck(customMetrics = []) {
16
+ return JSON.stringify({ type: 'ack-authorized', customMetrics });
17
+ }
11
18
  describe('WebSocketConnection', () => {
12
19
  let mockSocket;
13
20
  let connection;
14
21
  let logMessages;
15
22
  let mockLogger;
16
23
  beforeEach(() => {
17
- // Reset all mocks
18
24
  jest.clearAllMocks();
19
25
  jest.clearAllTimers();
20
26
  jest.useFakeTimers();
@@ -22,7 +28,6 @@ describe('WebSocketConnection', () => {
22
28
  mockLogger = jest.fn((message) => {
23
29
  logMessages.push(message);
24
30
  });
25
- // Create mock WebSocket instance with event handling
26
31
  mockSocket = {
27
32
  on: jest.fn(),
28
33
  send: jest.fn(),
@@ -35,7 +40,6 @@ describe('WebSocketConnection', () => {
35
40
  _triggerClose: function (code, reason) { },
36
41
  _triggerError: function (error) { }
37
42
  };
38
- // Set up event handler storage and triggering
39
43
  const eventHandlers = {};
40
44
  mockSocket.on.mockImplementation((event, handler) => {
41
45
  eventHandlers[event] = handler;
@@ -55,7 +59,6 @@ describe('WebSocketConnection', () => {
55
59
  mockSocket._triggerError = (error) => {
56
60
  eventHandlers['error']?.(error);
57
61
  };
58
- // Mock WebSocket constructor to return our mock instance
59
62
  MockedWebSocket.mockImplementation(() => mockSocket);
60
63
  });
61
64
  afterEach(() => {
@@ -66,25 +69,25 @@ describe('WebSocketConnection', () => {
66
69
  });
67
70
  describe('Connection Establishment', () => {
68
71
  it('should create WebSocket connection with correct URL', () => {
69
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
72
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
70
73
  expect(MockedWebSocket).toHaveBeenCalledWith('wss://ingestion.comprehend.dev/test-org/observations');
71
74
  expect(mockSocket.on).toHaveBeenCalledWith('open', expect.any(Function));
72
75
  expect(mockSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
73
76
  expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
74
77
  expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
75
78
  });
76
- it('should send init message on connection open', () => {
77
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
79
+ it('should send V2 init message on connection open', () => {
80
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
78
81
  mockSocket._triggerOpen();
79
82
  expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify({
80
83
  event: 'init',
81
- protocolVersion: 1,
84
+ protocolVersion: 2,
82
85
  token: 'test-token'
83
86
  }));
84
87
  expect(logMessages).toContain('WebSocket connected. Sending init/auth message.');
85
88
  });
86
89
  it('should work without logger', () => {
87
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token');
90
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token' });
88
91
  mockSocket._triggerOpen();
89
92
  expect(MockedWebSocket).toHaveBeenCalled();
90
93
  expect(mockSocket.send).toHaveBeenCalled();
@@ -92,19 +95,25 @@ describe('WebSocketConnection', () => {
92
95
  });
93
96
  describe('Authorization Flow', () => {
94
97
  beforeEach(() => {
95
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
98
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
96
99
  mockSocket._triggerOpen();
97
- jest.clearAllMocks(); // Clear the init message send
100
+ jest.clearAllMocks();
98
101
  });
99
- it('should handle authorization acknowledgment', () => {
100
- const authAck = {
101
- type: 'ack-authorized'
102
- };
103
- mockSocket._triggerMessage(JSON.stringify(authAck));
102
+ it('should handle authorization acknowledgment with customMetrics', () => {
103
+ mockSocket._triggerMessage(authAck([{ type: 'cumulative', id: 'test', attributes: [], subject: 'sub' }]));
104
104
  expect(logMessages).toContain('Authorization acknowledged by server.');
105
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
+ });
106
116
  it('should replay queued messages after authorization', () => {
107
- // Queue some messages before authorization
108
117
  const serviceMessage = {
109
118
  event: 'new-entity',
110
119
  type: 'service',
@@ -117,6 +126,8 @@ describe('WebSocketConnection', () => {
117
126
  observations: [{
118
127
  type: 'http-server',
119
128
  subject: 'test-subject',
129
+ spanId: 'span1',
130
+ traceId: 'trace1',
120
131
  timestamp: [1700000000, 0],
121
132
  path: '/test',
122
133
  status: 200,
@@ -125,18 +136,14 @@ describe('WebSocketConnection', () => {
125
136
  };
126
137
  connection.sendMessage(serviceMessage);
127
138
  connection.sendMessage(observationMessage);
128
- // Should not send immediately (not authorized yet)
129
139
  expect(mockSocket.send).not.toHaveBeenCalled();
130
- // Now authorize
131
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
132
- // Should replay both messages
140
+ mockSocket._triggerMessage(authAck());
133
141
  expect(mockSocket.send).toHaveBeenCalledTimes(2);
134
142
  expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
135
143
  expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
136
144
  });
137
145
  it('should send messages immediately when already authorized', () => {
138
- // First authorize
139
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
146
+ mockSocket._triggerMessage(authAck());
140
147
  jest.clearAllMocks();
141
148
  const serviceMessage = {
142
149
  event: 'new-entity',
@@ -148,129 +155,235 @@ describe('WebSocketConnection', () => {
148
155
  expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
149
156
  });
150
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
+ });
151
209
  describe('Message Acknowledgment Handling', () => {
152
210
  beforeEach(() => {
153
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
211
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
154
212
  mockSocket._triggerOpen();
155
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
213
+ mockSocket._triggerMessage(authAck());
156
214
  jest.clearAllMocks();
157
215
  });
158
216
  it('should handle entity/interaction acknowledgments', () => {
159
217
  const serviceMessage = {
160
- event: 'new-entity',
161
- type: 'service',
162
- hash: 'service-hash',
163
- name: 'test-service'
218
+ event: 'new-entity', type: 'service', hash: 'service-hash', name: 'test-service'
164
219
  };
165
220
  const routeMessage = {
166
- event: 'new-entity',
167
- type: 'http-route',
168
- hash: 'route-hash',
169
- parent: 'service-hash',
170
- method: 'GET',
171
- route: '/test'
221
+ event: 'new-entity', type: 'http-route', hash: 'route-hash',
222
+ parent: 'service-hash', method: 'GET', route: '/test'
172
223
  };
173
- // Send messages
174
224
  connection.sendMessage(serviceMessage);
175
225
  connection.sendMessage(routeMessage);
176
- // Acknowledge the service message
177
- mockSocket._triggerMessage(JSON.stringify({
178
- type: 'ack-observed',
179
- hash: 'service-hash'
180
- }));
181
- // Clear previous calls before reconnection test
226
+ // Acknowledge the service
227
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-observed', hash: 'service-hash' }));
182
228
  mockSocket.send.mockClear();
183
- // If we reconnect, only the route message should be replayed
229
+ // Reconnect only unacked route should replay
184
230
  mockSocket._triggerClose(1000, 'test close');
185
231
  jest.advanceTimersByTime(1000);
186
232
  mockSocket._triggerOpen();
187
- // Clear the init message that gets sent on reconnection
188
233
  mockSocket.send.mockClear();
189
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
190
- // Should only send the unacknowledged route message
234
+ mockSocket._triggerMessage(authAck());
191
235
  expect(mockSocket.send).toHaveBeenCalledTimes(1);
192
236
  expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(routeMessage));
193
237
  });
194
238
  it('should handle observation acknowledgments', () => {
195
- const observationMessage1 = {
196
- event: 'observations',
197
- seq: 1,
198
- observations: []
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: []
199
256
  };
200
- const observationMessage2 = {
201
- event: 'observations',
202
- seq: 2,
203
- observations: []
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]
204
299
  };
205
- // Send messages
206
- connection.sendMessage(observationMessage1);
207
- connection.sendMessage(observationMessage2);
208
- // Acknowledge first observation
209
- mockSocket._triggerMessage(JSON.stringify({
210
- type: 'ack-observations',
211
- seq: 1
212
- }));
213
- // Clear previous calls before reconnection test
300
+ connection.sendMessage(msg);
301
+ mockSocket._triggerMessage(JSON.stringify({ type: 'ack-db-query', seq: 9 }));
214
302
  mockSocket.send.mockClear();
215
- // Reconnect - only second observation should be replayed
216
- mockSocket._triggerClose(1000, 'test close');
303
+ mockSocket._triggerClose(1000, 'test');
217
304
  jest.advanceTimersByTime(1000);
218
305
  mockSocket._triggerOpen();
219
- // Clear the init message that gets sent on reconnection
220
306
  mockSocket.send.mockClear();
221
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
222
- // Should only send the unacknowledged second observation message
223
- expect(mockSocket.send).toHaveBeenCalledTimes(1);
224
- expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage2));
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);
225
349
  });
226
350
  });
227
351
  describe('Reconnection Logic', () => {
228
352
  beforeEach(() => {
229
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
353
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
230
354
  mockSocket._triggerOpen();
231
355
  });
232
356
  it('should reconnect automatically on close', () => {
233
357
  const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
234
358
  mockSocket._triggerClose(1000, 'Normal closure');
235
359
  expect(logMessages).toContain('WebSocket disconnected. Code: 1000, Reason: Normal closure');
236
- // Should schedule reconnection
237
360
  expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
238
- // Advance timer to trigger reconnection
239
361
  jest.advanceTimersByTime(1000);
240
- // Should create new WebSocket connection
241
362
  expect(MockedWebSocket).toHaveBeenCalledTimes(2);
242
363
  setTimeoutSpy.mockRestore();
243
364
  });
244
365
  it('should reset authorization state on close', () => {
245
- // First authorize
246
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
247
- // Send a message (should send immediately)
366
+ mockSocket._triggerMessage(authAck());
248
367
  const testMessage = {
249
- event: 'new-entity',
250
- type: 'service',
251
- hash: 'test-hash',
252
- name: 'test-service'
368
+ event: 'new-entity', type: 'service', hash: 'test-hash', name: 'test-service'
253
369
  };
254
370
  connection.sendMessage(testMessage);
255
371
  expect(mockSocket.send).toHaveBeenCalled();
256
372
  jest.clearAllMocks();
257
- // Close connection
258
373
  mockSocket._triggerClose(1000, 'test');
259
- // Send another message - should queue (not send immediately)
260
374
  connection.sendMessage(testMessage);
261
375
  expect(mockSocket.send).not.toHaveBeenCalled();
262
376
  });
263
377
  it('should not reconnect when explicitly closed', () => {
264
378
  connection.close();
265
379
  mockSocket._triggerClose(1000, 'Explicit close');
266
- // Should not schedule reconnection
267
380
  jest.advanceTimersByTime(5000);
268
- expect(MockedWebSocket).toHaveBeenCalledTimes(1); // Only initial connection
381
+ expect(MockedWebSocket).toHaveBeenCalledTimes(1);
269
382
  });
270
383
  });
271
384
  describe('Error Handling', () => {
272
385
  beforeEach(() => {
273
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
386
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
274
387
  });
275
388
  it('should log WebSocket errors', () => {
276
389
  const testError = new Error('Connection failed');
@@ -280,127 +393,81 @@ describe('WebSocketConnection', () => {
280
393
  it('should handle malformed server messages gracefully', () => {
281
394
  mockSocket._triggerOpen();
282
395
  mockSocket._triggerMessage('{invalid json}');
283
- // Check that an error message was logged (exact message varies by Node version)
284
396
  const errorLogExists = logMessages.some(msg => msg.startsWith('Error parsing message from server:') &&
285
397
  msg.includes('JSON'));
286
398
  expect(errorLogExists).toBe(true);
287
399
  });
288
- it('should handle non-string error objects in message parsing', () => {
289
- mockSocket._triggerOpen();
290
- // Mock JSON.parse to throw a non-Error object
291
- const originalParse = JSON.parse;
292
- JSON.parse = jest.fn().mockImplementation(() => {
293
- throw 'string error';
294
- });
295
- mockSocket._triggerMessage('{}');
296
- expect(logMessages).toContain('Error parsing message from server: string error');
297
- // Restore original JSON.parse
298
- JSON.parse = originalParse;
299
- });
300
400
  it('should not send messages when socket is not open', () => {
301
401
  mockSocket.readyState = ws_1.default.CLOSED;
302
402
  mockSocket._triggerOpen();
303
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
304
- // Clear the init and any previous messages
403
+ mockSocket._triggerMessage(authAck());
305
404
  mockSocket.send.mockClear();
306
- // Force socket to closed state
307
405
  mockSocket.readyState = ws_1.default.CLOSED;
308
406
  const testMessage = {
309
- event: 'new-entity',
310
- type: 'service',
311
- hash: 'test-hash',
312
- name: 'test-service'
407
+ event: 'new-entity', type: 'service', hash: 'test-hash', name: 'test-service'
313
408
  };
314
409
  connection.sendMessage(testMessage);
315
- // Should not call send when socket is closed
316
410
  expect(mockSocket.send).not.toHaveBeenCalled();
317
411
  });
318
412
  });
319
413
  describe('Connection Lifecycle', () => {
320
414
  it('should close WebSocket connection properly', () => {
321
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
415
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
322
416
  connection.close();
323
417
  expect(mockSocket.close).toHaveBeenCalled();
324
418
  });
325
419
  it('should handle close when socket is null', () => {
326
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
327
- // Simulate socket being null
420
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
328
421
  connection.socket = null;
329
422
  expect(() => connection.close()).not.toThrow();
330
423
  });
331
424
  });
332
- describe('Message Types and Queuing', () => {
425
+ describe('Message Queuing for All Types', () => {
333
426
  beforeEach(() => {
334
- connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
427
+ connection = new WebSocketConnection_1.WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
335
428
  mockSocket._triggerOpen();
336
- });
337
- it('should queue new-entity messages', () => {
338
- // Clear the initial init message
339
429
  mockSocket.send.mockClear();
340
- const serviceMessage = {
341
- event: 'new-entity',
342
- type: 'service',
343
- hash: 'service-hash',
344
- name: 'test-service'
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
+ ]
345
436
  };
346
- connection.sendMessage(serviceMessage);
347
- // Should be queued, not sent immediately (not authorized)
437
+ connection.sendMessage(msg);
348
438
  expect(mockSocket.send).not.toHaveBeenCalled();
349
- // Authorize and check replay
350
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
351
- expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
439
+ mockSocket._triggerMessage(authAck());
440
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
352
441
  });
353
- it('should queue new-interaction messages', () => {
354
- // Clear the initial init message
355
- mockSocket.send.mockClear();
356
- const httpRequestMessage = {
357
- event: 'new-interaction',
358
- type: 'http-request',
359
- hash: 'request-hash',
360
- from: 'service-hash',
361
- to: 'http-service-hash'
442
+ it('should queue and replay cumulative messages', () => {
443
+ const msg = {
444
+ event: 'cumulative', seq: 2, data: []
362
445
  };
363
- connection.sendMessage(httpRequestMessage);
364
- // Should be queued
446
+ connection.sendMessage(msg);
365
447
  expect(mockSocket.send).not.toHaveBeenCalled();
366
- // Authorize and check replay
367
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
368
- expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(httpRequestMessage));
448
+ mockSocket._triggerMessage(authAck());
449
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
369
450
  });
370
- it('should queue observation messages', () => {
371
- // Clear the initial init message
372
- mockSocket.send.mockClear();
373
- const observationMessage = {
374
- event: 'observations',
375
- seq: 1,
376
- observations: [{
377
- type: 'http-server',
378
- subject: 'route-hash',
379
- timestamp: [1700000000, 0],
380
- path: '/test',
381
- status: 200,
382
- duration: [0, 50000000]
383
- }]
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
+ ]
384
456
  };
385
- connection.sendMessage(observationMessage);
386
- // Should be queued
457
+ connection.sendMessage(msg);
387
458
  expect(mockSocket.send).not.toHaveBeenCalled();
388
- // Authorize and check replay
389
- mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
390
- expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
459
+ mockSocket._triggerMessage(authAck());
460
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
391
461
  });
392
- it('should not queue init messages in unacknowledged maps', () => {
393
- // This is implicit - init messages are sent via sendRaw, not sendMessage
394
- // and don't go through the queuing logic
395
- const initMessage = {
396
- event: 'init',
397
- protocolVersion: 1,
398
- token: 'test-token'
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]
399
466
  };
400
- // Manually test sendRaw behavior (private method testing)
401
- mockSocket.readyState = ws_1.default.OPEN;
402
- connection.sendRaw(initMessage);
403
- expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(initMessage));
467
+ connection.sendMessage(msg);
468
+ expect(mockSocket.send).not.toHaveBeenCalled();
469
+ mockSocket._triggerMessage(authAck());
470
+ expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
404
471
  });
405
472
  });
406
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; } });