@comprehend/telemetry-node 0.1.4 → 0.2.1
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/README.md +112 -27
- package/dist/ComprehendDevSpanProcessor.d.ts +10 -6
- package/dist/ComprehendDevSpanProcessor.js +154 -87
- package/dist/ComprehendDevSpanProcessor.test.js +270 -449
- package/dist/ComprehendMetricsExporter.d.ts +18 -0
- package/dist/ComprehendMetricsExporter.js +178 -0
- package/dist/ComprehendMetricsExporter.test.d.ts +1 -0
- package/dist/ComprehendMetricsExporter.test.js +266 -0
- package/dist/ComprehendSDK.d.ts +18 -0
- package/dist/ComprehendSDK.js +56 -0
- package/dist/ComprehendSDK.test.d.ts +1 -0
- package/dist/ComprehendSDK.test.js +126 -0
- package/dist/WebSocketConnection.d.ts +23 -3
- package/dist/WebSocketConnection.js +106 -12
- package/dist/WebSocketConnection.test.js +236 -169
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +7 -0
- package/dist/wire-protocol.d.ts +168 -28
- package/package.json +3 -1
- package/src/ComprehendDevSpanProcessor.test.ts +311 -507
- package/src/ComprehendDevSpanProcessor.ts +178 -105
- package/src/ComprehendMetricsExporter.test.ts +334 -0
- package/src/ComprehendMetricsExporter.ts +225 -0
- package/src/ComprehendSDK.test.ts +160 -0
- package/src/ComprehendSDK.ts +63 -0
- package/src/WebSocketConnection.test.ts +286 -205
- package/src/WebSocketConnection.ts +135 -13
- package/src/index.ts +3 -2
- package/src/util.ts +6 -0
- package/src/wire-protocol.ts +204 -29
- package/src/sql-analyzer.test.ts +0 -599
- package/src/sql-analyzer.ts +0 -439
|
@@ -5,12 +5,23 @@ import {
|
|
|
5
5
|
NewObservedHttpRouteMessage,
|
|
6
6
|
ObservationMessage,
|
|
7
7
|
HttpServerObservation,
|
|
8
|
-
ObservationOutputMessage
|
|
8
|
+
ObservationOutputMessage,
|
|
9
|
+
TimeSeriesMetricsMessage,
|
|
10
|
+
CumulativeMetricsMessage,
|
|
11
|
+
TraceSpansMessage,
|
|
12
|
+
DatabaseQueryMessage,
|
|
13
|
+
InitAck,
|
|
14
|
+
CustomMetricSpecification,
|
|
9
15
|
} from './wire-protocol';
|
|
10
16
|
|
|
11
17
|
// Mock the ws WebSocket library
|
|
12
18
|
jest.mock('ws');
|
|
13
19
|
|
|
20
|
+
// Mock crypto.randomUUID
|
|
21
|
+
jest.mock('crypto', () => ({
|
|
22
|
+
randomUUID: jest.fn().mockReturnValue('test-uuid-1234'),
|
|
23
|
+
}));
|
|
24
|
+
|
|
14
25
|
import WebSocket from 'ws';
|
|
15
26
|
|
|
16
27
|
const MockedWebSocket = WebSocket as jest.MockedClass<typeof WebSocket>;
|
|
@@ -23,13 +34,16 @@ interface MockWebSocketInstance {
|
|
|
23
34
|
readyState: number;
|
|
24
35
|
OPEN: number;
|
|
25
36
|
CLOSED: number;
|
|
26
|
-
// Event handlers that tests can trigger
|
|
27
37
|
_triggerOpen: () => void;
|
|
28
38
|
_triggerMessage: (data: string) => void;
|
|
29
39
|
_triggerClose: (code: number, reason: string) => void;
|
|
30
40
|
_triggerError: (error: Error) => void;
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
function authAck(customMetrics: CustomMetricSpecification[] = []): string {
|
|
44
|
+
return JSON.stringify({ type: 'ack-authorized', customMetrics } as InitAck);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
describe('WebSocketConnection', () => {
|
|
34
48
|
let mockSocket: MockWebSocketInstance;
|
|
35
49
|
let connection: WebSocketConnection;
|
|
@@ -37,7 +51,6 @@ describe('WebSocketConnection', () => {
|
|
|
37
51
|
let mockLogger: jest.Mock;
|
|
38
52
|
|
|
39
53
|
beforeEach(() => {
|
|
40
|
-
// Reset all mocks
|
|
41
54
|
jest.clearAllMocks();
|
|
42
55
|
jest.clearAllTimers();
|
|
43
56
|
jest.useFakeTimers();
|
|
@@ -47,7 +60,6 @@ describe('WebSocketConnection', () => {
|
|
|
47
60
|
logMessages.push(message);
|
|
48
61
|
});
|
|
49
62
|
|
|
50
|
-
// Create mock WebSocket instance with event handling
|
|
51
63
|
mockSocket = {
|
|
52
64
|
on: jest.fn(),
|
|
53
65
|
send: jest.fn(),
|
|
@@ -55,13 +67,12 @@ describe('WebSocketConnection', () => {
|
|
|
55
67
|
readyState: WebSocket.CLOSED,
|
|
56
68
|
OPEN: WebSocket.OPEN,
|
|
57
69
|
CLOSED: WebSocket.CLOSED,
|
|
58
|
-
_triggerOpen: function() {
|
|
59
|
-
_triggerMessage: function(data: string) {
|
|
60
|
-
_triggerClose: function(code: number, reason: string) {
|
|
61
|
-
_triggerError: function(error: Error) {
|
|
70
|
+
_triggerOpen: function() {},
|
|
71
|
+
_triggerMessage: function(data: string) {},
|
|
72
|
+
_triggerClose: function(code: number, reason: string) {},
|
|
73
|
+
_triggerError: function(error: Error) {}
|
|
62
74
|
};
|
|
63
75
|
|
|
64
|
-
// Set up event handler storage and triggering
|
|
65
76
|
const eventHandlers: Record<string, Function> = {};
|
|
66
77
|
mockSocket.on.mockImplementation((event: string, handler: Function) => {
|
|
67
78
|
eventHandlers[event] = handler;
|
|
@@ -83,7 +94,6 @@ describe('WebSocketConnection', () => {
|
|
|
83
94
|
eventHandlers['error']?.(error);
|
|
84
95
|
};
|
|
85
96
|
|
|
86
|
-
// Mock WebSocket constructor to return our mock instance
|
|
87
97
|
MockedWebSocket.mockImplementation(() => mockSocket as any);
|
|
88
98
|
});
|
|
89
99
|
|
|
@@ -96,7 +106,7 @@ describe('WebSocketConnection', () => {
|
|
|
96
106
|
|
|
97
107
|
describe('Connection Establishment', () => {
|
|
98
108
|
it('should create WebSocket connection with correct URL', () => {
|
|
99
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
109
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
100
110
|
|
|
101
111
|
expect(MockedWebSocket).toHaveBeenCalledWith('wss://ingestion.comprehend.dev/test-org/observations');
|
|
102
112
|
expect(mockSocket.on).toHaveBeenCalledWith('open', expect.any(Function));
|
|
@@ -105,21 +115,21 @@ describe('WebSocketConnection', () => {
|
|
|
105
115
|
expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
106
116
|
});
|
|
107
117
|
|
|
108
|
-
it('should send init message on connection open', () => {
|
|
109
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
118
|
+
it('should send V2 init message on connection open', () => {
|
|
119
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
110
120
|
|
|
111
121
|
mockSocket._triggerOpen();
|
|
112
122
|
|
|
113
123
|
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify({
|
|
114
124
|
event: 'init',
|
|
115
|
-
protocolVersion:
|
|
125
|
+
protocolVersion: 2,
|
|
116
126
|
token: 'test-token'
|
|
117
127
|
}));
|
|
118
128
|
expect(logMessages).toContain('WebSocket connected. Sending init/auth message.');
|
|
119
129
|
});
|
|
120
130
|
|
|
121
131
|
it('should work without logger', () => {
|
|
122
|
-
connection = new WebSocketConnection('test-org', 'test-token');
|
|
132
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token' });
|
|
123
133
|
|
|
124
134
|
mockSocket._triggerOpen();
|
|
125
135
|
|
|
@@ -130,23 +140,31 @@ describe('WebSocketConnection', () => {
|
|
|
130
140
|
|
|
131
141
|
describe('Authorization Flow', () => {
|
|
132
142
|
beforeEach(() => {
|
|
133
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
143
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
134
144
|
mockSocket._triggerOpen();
|
|
135
|
-
jest.clearAllMocks();
|
|
145
|
+
jest.clearAllMocks();
|
|
136
146
|
});
|
|
137
147
|
|
|
138
|
-
it('should handle authorization acknowledgment', () => {
|
|
139
|
-
|
|
140
|
-
type: 'ack-authorized'
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
mockSocket._triggerMessage(JSON.stringify(authAck));
|
|
148
|
+
it('should handle authorization acknowledgment with customMetrics', () => {
|
|
149
|
+
mockSocket._triggerMessage(authAck([{ type: 'cumulative', id: 'test', attributes: [], subject: 'sub' }]));
|
|
144
150
|
|
|
145
151
|
expect(logMessages).toContain('Authorization acknowledged by server.');
|
|
146
152
|
});
|
|
147
153
|
|
|
154
|
+
it('should call onAuthorized callback with InitAck', () => {
|
|
155
|
+
const onAuthorized = jest.fn();
|
|
156
|
+
connection = new WebSocketConnection({
|
|
157
|
+
organization: 'test-org', token: 'test-token', logger: mockLogger, onAuthorized
|
|
158
|
+
});
|
|
159
|
+
mockSocket._triggerOpen();
|
|
160
|
+
|
|
161
|
+
const customMetrics: CustomMetricSpecification[] = [{ type: 'cumulative', id: 'test', attributes: [], subject: 'sub' }];
|
|
162
|
+
mockSocket._triggerMessage(authAck(customMetrics));
|
|
163
|
+
|
|
164
|
+
expect(onAuthorized).toHaveBeenCalledWith({ type: 'ack-authorized', customMetrics });
|
|
165
|
+
});
|
|
166
|
+
|
|
148
167
|
it('should replay queued messages after authorization', () => {
|
|
149
|
-
// Queue some messages before authorization
|
|
150
168
|
const serviceMessage: NewObservedServiceMessage = {
|
|
151
169
|
event: 'new-entity',
|
|
152
170
|
type: 'service',
|
|
@@ -160,6 +178,8 @@ describe('WebSocketConnection', () => {
|
|
|
160
178
|
observations: [{
|
|
161
179
|
type: 'http-server',
|
|
162
180
|
subject: 'test-subject',
|
|
181
|
+
spanId: 'span1',
|
|
182
|
+
traceId: 'trace1',
|
|
163
183
|
timestamp: [1700000000, 0],
|
|
164
184
|
path: '/test',
|
|
165
185
|
status: 200,
|
|
@@ -170,21 +190,17 @@ describe('WebSocketConnection', () => {
|
|
|
170
190
|
connection.sendMessage(serviceMessage);
|
|
171
191
|
connection.sendMessage(observationMessage);
|
|
172
192
|
|
|
173
|
-
// Should not send immediately (not authorized yet)
|
|
174
193
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
175
194
|
|
|
176
|
-
|
|
177
|
-
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
195
|
+
mockSocket._triggerMessage(authAck());
|
|
178
196
|
|
|
179
|
-
// Should replay both messages
|
|
180
197
|
expect(mockSocket.send).toHaveBeenCalledTimes(2);
|
|
181
198
|
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
182
199
|
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
|
|
183
200
|
});
|
|
184
201
|
|
|
185
202
|
it('should send messages immediately when already authorized', () => {
|
|
186
|
-
|
|
187
|
-
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
203
|
+
mockSocket._triggerMessage(authAck());
|
|
188
204
|
jest.clearAllMocks();
|
|
189
205
|
|
|
190
206
|
const serviceMessage: NewObservedServiceMessage = {
|
|
@@ -200,104 +216,251 @@ describe('WebSocketConnection', () => {
|
|
|
200
216
|
});
|
|
201
217
|
});
|
|
202
218
|
|
|
219
|
+
describe('Context Handling', () => {
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
222
|
+
mockSocket._triggerOpen();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should send context-start after auth when process context is set before auth', () => {
|
|
226
|
+
connection.setProcessContext('service-hash', { 'service.name': 'test' });
|
|
227
|
+
|
|
228
|
+
// Not authorized yet, so no context-start sent
|
|
229
|
+
mockSocket.send.mockClear();
|
|
230
|
+
|
|
231
|
+
mockSocket._triggerMessage(authAck());
|
|
232
|
+
|
|
233
|
+
// Should have sent context-start first
|
|
234
|
+
const sentMessages = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]));
|
|
235
|
+
const contextMsg = sentMessages.find((m: any) => m.event === 'context-start');
|
|
236
|
+
expect(contextMsg).toMatchObject({
|
|
237
|
+
event: 'context-start',
|
|
238
|
+
type: 'process',
|
|
239
|
+
serviceEntityHash: 'service-hash',
|
|
240
|
+
resources: { 'service.name': 'test' },
|
|
241
|
+
ingestionId: expect.any(String),
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should send context-start immediately when already authorized', () => {
|
|
246
|
+
mockSocket._triggerMessage(authAck());
|
|
247
|
+
mockSocket.send.mockClear();
|
|
248
|
+
|
|
249
|
+
connection.setProcessContext('service-hash', { 'service.name': 'test' });
|
|
250
|
+
|
|
251
|
+
const sentMessages = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]));
|
|
252
|
+
expect(sentMessages[0]).toMatchObject({
|
|
253
|
+
event: 'context-start',
|
|
254
|
+
type: 'process',
|
|
255
|
+
serviceEntityHash: 'service-hash',
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should re-send context-start on reconnect before other replays', () => {
|
|
260
|
+
connection.setProcessContext('service-hash', { 'service.name': 'test' });
|
|
261
|
+
mockSocket._triggerMessage(authAck());
|
|
262
|
+
|
|
263
|
+
// Send an entity message
|
|
264
|
+
const entityMsg: NewObservedServiceMessage = {
|
|
265
|
+
event: 'new-entity', type: 'service', hash: 'h1', name: 'svc'
|
|
266
|
+
};
|
|
267
|
+
connection.sendMessage(entityMsg);
|
|
268
|
+
|
|
269
|
+
// Reconnect
|
|
270
|
+
mockSocket._triggerClose(1000, 'test');
|
|
271
|
+
jest.advanceTimersByTime(1000);
|
|
272
|
+
mockSocket._triggerOpen();
|
|
273
|
+
mockSocket.send.mockClear();
|
|
274
|
+
|
|
275
|
+
mockSocket._triggerMessage(authAck());
|
|
276
|
+
|
|
277
|
+
const sentMessages = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]));
|
|
278
|
+
// Context should come first
|
|
279
|
+
expect(sentMessages[0].event).toBe('context-start');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
203
283
|
describe('Message Acknowledgment Handling', () => {
|
|
204
284
|
beforeEach(() => {
|
|
205
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
285
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
206
286
|
mockSocket._triggerOpen();
|
|
207
|
-
mockSocket._triggerMessage(
|
|
287
|
+
mockSocket._triggerMessage(authAck());
|
|
208
288
|
jest.clearAllMocks();
|
|
209
289
|
});
|
|
210
290
|
|
|
211
291
|
it('should handle entity/interaction acknowledgments', () => {
|
|
212
292
|
const serviceMessage: NewObservedServiceMessage = {
|
|
213
|
-
event: 'new-entity',
|
|
214
|
-
type: 'service',
|
|
215
|
-
hash: 'service-hash',
|
|
216
|
-
name: 'test-service'
|
|
293
|
+
event: 'new-entity', type: 'service', hash: 'service-hash', name: 'test-service'
|
|
217
294
|
};
|
|
218
|
-
|
|
219
295
|
const routeMessage: NewObservedHttpRouteMessage = {
|
|
220
|
-
event: 'new-entity',
|
|
221
|
-
|
|
222
|
-
hash: 'route-hash',
|
|
223
|
-
parent: 'service-hash',
|
|
224
|
-
method: 'GET',
|
|
225
|
-
route: '/test'
|
|
296
|
+
event: 'new-entity', type: 'http-route', hash: 'route-hash',
|
|
297
|
+
parent: 'service-hash', method: 'GET', route: '/test'
|
|
226
298
|
};
|
|
227
299
|
|
|
228
|
-
// Send messages
|
|
229
300
|
connection.sendMessage(serviceMessage);
|
|
230
301
|
connection.sendMessage(routeMessage);
|
|
231
302
|
|
|
232
|
-
// Acknowledge the service
|
|
233
|
-
mockSocket._triggerMessage(JSON.stringify({
|
|
234
|
-
type: 'ack-observed',
|
|
235
|
-
hash: 'service-hash'
|
|
236
|
-
}));
|
|
303
|
+
// Acknowledge the service
|
|
304
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-observed', hash: 'service-hash' }));
|
|
237
305
|
|
|
238
|
-
// Clear previous calls before reconnection test
|
|
239
306
|
mockSocket.send.mockClear();
|
|
240
307
|
|
|
241
|
-
//
|
|
308
|
+
// Reconnect — only unacked route should replay
|
|
242
309
|
mockSocket._triggerClose(1000, 'test close');
|
|
243
310
|
jest.advanceTimersByTime(1000);
|
|
244
311
|
mockSocket._triggerOpen();
|
|
245
|
-
|
|
246
|
-
// Clear the init message that gets sent on reconnection
|
|
247
312
|
mockSocket.send.mockClear();
|
|
248
313
|
|
|
249
|
-
mockSocket._triggerMessage(
|
|
314
|
+
mockSocket._triggerMessage(authAck());
|
|
250
315
|
|
|
251
|
-
// Should only send the unacknowledged route message
|
|
252
316
|
expect(mockSocket.send).toHaveBeenCalledTimes(1);
|
|
253
317
|
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(routeMessage));
|
|
254
318
|
});
|
|
255
319
|
|
|
256
320
|
it('should handle observation acknowledgments', () => {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
seq: 1,
|
|
260
|
-
observations: []
|
|
261
|
-
};
|
|
321
|
+
const obs1: ObservationMessage = { event: 'observations', seq: 1, observations: [] };
|
|
322
|
+
const obs2: ObservationMessage = { event: 'observations', seq: 2, observations: [] };
|
|
262
323
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
324
|
+
connection.sendMessage(obs1);
|
|
325
|
+
connection.sendMessage(obs2);
|
|
326
|
+
|
|
327
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-observations', seq: 1 }));
|
|
328
|
+
|
|
329
|
+
mockSocket.send.mockClear();
|
|
330
|
+
mockSocket._triggerClose(1000, 'test');
|
|
331
|
+
jest.advanceTimersByTime(1000);
|
|
332
|
+
mockSocket._triggerOpen();
|
|
333
|
+
mockSocket.send.mockClear();
|
|
334
|
+
|
|
335
|
+
mockSocket._triggerMessage(authAck());
|
|
336
|
+
|
|
337
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(1);
|
|
338
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(obs2));
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should handle timeseries acknowledgments', () => {
|
|
342
|
+
const msg: TimeSeriesMetricsMessage = {
|
|
343
|
+
event: 'timeseries', seq: 5, data: []
|
|
267
344
|
};
|
|
345
|
+
connection.sendMessage(msg);
|
|
268
346
|
|
|
269
|
-
|
|
270
|
-
connection.sendMessage(observationMessage1);
|
|
271
|
-
connection.sendMessage(observationMessage2);
|
|
347
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-timeseries', seq: 5 }));
|
|
272
348
|
|
|
273
|
-
|
|
274
|
-
mockSocket.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
349
|
+
mockSocket.send.mockClear();
|
|
350
|
+
mockSocket._triggerClose(1000, 'test');
|
|
351
|
+
jest.advanceTimersByTime(1000);
|
|
352
|
+
mockSocket._triggerOpen();
|
|
353
|
+
mockSocket.send.mockClear();
|
|
354
|
+
mockSocket._triggerMessage(authAck());
|
|
355
|
+
|
|
356
|
+
// Should not replay the acked message
|
|
357
|
+
const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
|
|
358
|
+
expect(sentEvents).not.toContain('timeseries');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should handle cumulative acknowledgments', () => {
|
|
362
|
+
const msg: CumulativeMetricsMessage = { event: 'cumulative', seq: 3, data: [] };
|
|
363
|
+
connection.sendMessage(msg);
|
|
364
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-cumulative', seq: 3 }));
|
|
278
365
|
|
|
279
|
-
// Clear previous calls before reconnection test
|
|
280
366
|
mockSocket.send.mockClear();
|
|
367
|
+
mockSocket._triggerClose(1000, 'test');
|
|
368
|
+
jest.advanceTimersByTime(1000);
|
|
369
|
+
mockSocket._triggerOpen();
|
|
370
|
+
mockSocket.send.mockClear();
|
|
371
|
+
mockSocket._triggerMessage(authAck());
|
|
281
372
|
|
|
282
|
-
|
|
283
|
-
|
|
373
|
+
const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
|
|
374
|
+
expect(sentEvents).not.toContain('cumulative');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should handle tracespans acknowledgments', () => {
|
|
378
|
+
const msg: TraceSpansMessage = { event: 'tracespans', seq: 7, data: [] };
|
|
379
|
+
connection.sendMessage(msg);
|
|
380
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-tracespans', seq: 7 }));
|
|
381
|
+
|
|
382
|
+
mockSocket.send.mockClear();
|
|
383
|
+
mockSocket._triggerClose(1000, 'test');
|
|
284
384
|
jest.advanceTimersByTime(1000);
|
|
285
385
|
mockSocket._triggerOpen();
|
|
386
|
+
mockSocket.send.mockClear();
|
|
387
|
+
mockSocket._triggerMessage(authAck());
|
|
388
|
+
|
|
389
|
+
const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
|
|
390
|
+
expect(sentEvents).not.toContain('tracespans');
|
|
391
|
+
});
|
|
286
392
|
|
|
287
|
-
|
|
393
|
+
it('should handle db-query acknowledgments', () => {
|
|
394
|
+
const msg: DatabaseQueryMessage = {
|
|
395
|
+
event: 'db-query', seq: 9, query: 'SELECT 1', from: 'a', to: 'b',
|
|
396
|
+
timestamp: [0, 0], duration: [0, 0]
|
|
397
|
+
};
|
|
398
|
+
connection.sendMessage(msg);
|
|
399
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-db-query', seq: 9 }));
|
|
400
|
+
|
|
401
|
+
mockSocket.send.mockClear();
|
|
402
|
+
mockSocket._triggerClose(1000, 'test');
|
|
403
|
+
jest.advanceTimersByTime(1000);
|
|
404
|
+
mockSocket._triggerOpen();
|
|
288
405
|
mockSocket.send.mockClear();
|
|
406
|
+
mockSocket._triggerMessage(authAck());
|
|
289
407
|
|
|
290
|
-
mockSocket.
|
|
408
|
+
const sentEvents = mockSocket.send.mock.calls.map(c => JSON.parse(c[0]).event);
|
|
409
|
+
expect(sentEvents).not.toContain('db-query');
|
|
410
|
+
});
|
|
291
411
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
412
|
+
it('should handle context acknowledgments', () => {
|
|
413
|
+
connection.setProcessContext('svc-hash', { 'service.name': 'test' });
|
|
414
|
+
mockSocket._triggerMessage(authAck());
|
|
415
|
+
|
|
416
|
+
// Find the context seq
|
|
417
|
+
const contextCall = mockSocket.send.mock.calls.find(c => {
|
|
418
|
+
const m = JSON.parse(c[0]);
|
|
419
|
+
return m.event === 'context-start';
|
|
420
|
+
});
|
|
421
|
+
const contextSeq = JSON.parse(contextCall![0]).seq;
|
|
422
|
+
|
|
423
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-context', seq: contextSeq }));
|
|
424
|
+
|
|
425
|
+
// On reconnect, context-start should still be sent (because setProcessContext data persists)
|
|
426
|
+
// but the acked one should be removed from the unack queue
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('Custom Metric Change', () => {
|
|
431
|
+
it('should call onCustomMetricChange callback', () => {
|
|
432
|
+
const onCustomMetricChange = jest.fn();
|
|
433
|
+
connection = new WebSocketConnection({
|
|
434
|
+
organization: 'test-org', token: 'test-token', onCustomMetricChange
|
|
435
|
+
});
|
|
436
|
+
mockSocket._triggerOpen();
|
|
437
|
+
mockSocket._triggerMessage(authAck());
|
|
438
|
+
|
|
439
|
+
const specs: CustomMetricSpecification[] = [
|
|
440
|
+
{ type: 'timeseries', id: 'metric1', attributes: ['a'], subject: 'sub1' }
|
|
441
|
+
];
|
|
442
|
+
mockSocket._triggerMessage(JSON.stringify({
|
|
443
|
+
type: 'custom-metric-change',
|
|
444
|
+
customMetrics: specs
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
expect(onCustomMetricChange).toHaveBeenCalledWith(specs);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('Shared Sequence Counter', () => {
|
|
452
|
+
it('should provide incrementing sequence numbers', () => {
|
|
453
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token' });
|
|
454
|
+
|
|
455
|
+
expect(connection.nextSeq()).toBe(1);
|
|
456
|
+
expect(connection.nextSeq()).toBe(2);
|
|
457
|
+
expect(connection.nextSeq()).toBe(3);
|
|
295
458
|
});
|
|
296
459
|
});
|
|
297
460
|
|
|
298
461
|
describe('Reconnection Logic', () => {
|
|
299
462
|
beforeEach(() => {
|
|
300
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
463
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
301
464
|
mockSocket._triggerOpen();
|
|
302
465
|
});
|
|
303
466
|
|
|
@@ -307,39 +470,26 @@ describe('WebSocketConnection', () => {
|
|
|
307
470
|
mockSocket._triggerClose(1000, 'Normal closure');
|
|
308
471
|
|
|
309
472
|
expect(logMessages).toContain('WebSocket disconnected. Code: 1000, Reason: Normal closure');
|
|
310
|
-
|
|
311
|
-
// Should schedule reconnection
|
|
312
473
|
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
|
|
313
474
|
|
|
314
|
-
// Advance timer to trigger reconnection
|
|
315
475
|
jest.advanceTimersByTime(1000);
|
|
316
|
-
|
|
317
|
-
// Should create new WebSocket connection
|
|
318
476
|
expect(MockedWebSocket).toHaveBeenCalledTimes(2);
|
|
319
477
|
|
|
320
478
|
setTimeoutSpy.mockRestore();
|
|
321
479
|
});
|
|
322
480
|
|
|
323
481
|
it('should reset authorization state on close', () => {
|
|
324
|
-
|
|
325
|
-
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
482
|
+
mockSocket._triggerMessage(authAck());
|
|
326
483
|
|
|
327
|
-
// Send a message (should send immediately)
|
|
328
484
|
const testMessage: NewObservedServiceMessage = {
|
|
329
|
-
event: 'new-entity',
|
|
330
|
-
type: 'service',
|
|
331
|
-
hash: 'test-hash',
|
|
332
|
-
name: 'test-service'
|
|
485
|
+
event: 'new-entity', type: 'service', hash: 'test-hash', name: 'test-service'
|
|
333
486
|
};
|
|
334
487
|
connection.sendMessage(testMessage);
|
|
335
488
|
expect(mockSocket.send).toHaveBeenCalled();
|
|
336
489
|
|
|
337
490
|
jest.clearAllMocks();
|
|
338
|
-
|
|
339
|
-
// Close connection
|
|
340
491
|
mockSocket._triggerClose(1000, 'test');
|
|
341
492
|
|
|
342
|
-
// Send another message - should queue (not send immediately)
|
|
343
493
|
connection.sendMessage(testMessage);
|
|
344
494
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
345
495
|
});
|
|
@@ -348,15 +498,14 @@ describe('WebSocketConnection', () => {
|
|
|
348
498
|
connection.close();
|
|
349
499
|
mockSocket._triggerClose(1000, 'Explicit close');
|
|
350
500
|
|
|
351
|
-
// Should not schedule reconnection
|
|
352
501
|
jest.advanceTimersByTime(5000);
|
|
353
|
-
expect(MockedWebSocket).toHaveBeenCalledTimes(1);
|
|
502
|
+
expect(MockedWebSocket).toHaveBeenCalledTimes(1);
|
|
354
503
|
});
|
|
355
504
|
});
|
|
356
505
|
|
|
357
506
|
describe('Error Handling', () => {
|
|
358
507
|
beforeEach(() => {
|
|
359
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
508
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
360
509
|
});
|
|
361
510
|
|
|
362
511
|
it('should log WebSocket errors', () => {
|
|
@@ -370,7 +519,6 @@ describe('WebSocketConnection', () => {
|
|
|
370
519
|
mockSocket._triggerOpen();
|
|
371
520
|
mockSocket._triggerMessage('{invalid json}');
|
|
372
521
|
|
|
373
|
-
// Check that an error message was logged (exact message varies by Node version)
|
|
374
522
|
const errorLogExists = logMessages.some(msg =>
|
|
375
523
|
msg.startsWith('Error parsing message from server:') &&
|
|
376
524
|
msg.includes('JSON')
|
|
@@ -378,158 +526,91 @@ describe('WebSocketConnection', () => {
|
|
|
378
526
|
expect(errorLogExists).toBe(true);
|
|
379
527
|
});
|
|
380
528
|
|
|
381
|
-
it('should handle non-string error objects in message parsing', () => {
|
|
382
|
-
mockSocket._triggerOpen();
|
|
383
|
-
|
|
384
|
-
// Mock JSON.parse to throw a non-Error object
|
|
385
|
-
const originalParse = JSON.parse;
|
|
386
|
-
JSON.parse = jest.fn().mockImplementation(() => {
|
|
387
|
-
throw 'string error';
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
mockSocket._triggerMessage('{}');
|
|
391
|
-
|
|
392
|
-
expect(logMessages).toContain('Error parsing message from server: string error');
|
|
393
|
-
|
|
394
|
-
// Restore original JSON.parse
|
|
395
|
-
JSON.parse = originalParse;
|
|
396
|
-
});
|
|
397
|
-
|
|
398
529
|
it('should not send messages when socket is not open', () => {
|
|
399
530
|
mockSocket.readyState = WebSocket.CLOSED;
|
|
400
531
|
mockSocket._triggerOpen();
|
|
401
|
-
mockSocket._triggerMessage(
|
|
402
|
-
|
|
403
|
-
// Clear the init and any previous messages
|
|
532
|
+
mockSocket._triggerMessage(authAck());
|
|
404
533
|
mockSocket.send.mockClear();
|
|
405
534
|
|
|
406
|
-
// Force socket to closed state
|
|
407
535
|
mockSocket.readyState = WebSocket.CLOSED;
|
|
408
536
|
|
|
409
537
|
const testMessage: NewObservedServiceMessage = {
|
|
410
|
-
event: 'new-entity',
|
|
411
|
-
type: 'service',
|
|
412
|
-
hash: 'test-hash',
|
|
413
|
-
name: 'test-service'
|
|
538
|
+
event: 'new-entity', type: 'service', hash: 'test-hash', name: 'test-service'
|
|
414
539
|
};
|
|
415
|
-
|
|
416
540
|
connection.sendMessage(testMessage);
|
|
417
541
|
|
|
418
|
-
// Should not call send when socket is closed
|
|
419
542
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
420
543
|
});
|
|
421
544
|
});
|
|
422
545
|
|
|
423
546
|
describe('Connection Lifecycle', () => {
|
|
424
547
|
it('should close WebSocket connection properly', () => {
|
|
425
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
426
|
-
|
|
548
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
427
549
|
connection.close();
|
|
428
|
-
|
|
429
550
|
expect(mockSocket.close).toHaveBeenCalled();
|
|
430
551
|
});
|
|
431
552
|
|
|
432
553
|
it('should handle close when socket is null', () => {
|
|
433
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
434
|
-
|
|
435
|
-
// Simulate socket being null
|
|
554
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
436
555
|
(connection as any).socket = null;
|
|
437
|
-
|
|
438
556
|
expect(() => connection.close()).not.toThrow();
|
|
439
557
|
});
|
|
440
558
|
});
|
|
441
559
|
|
|
442
|
-
describe('Message
|
|
560
|
+
describe('Message Queuing for All Types', () => {
|
|
443
561
|
beforeEach(() => {
|
|
444
|
-
connection = new WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
562
|
+
connection = new WebSocketConnection({ organization: 'test-org', token: 'test-token', logger: mockLogger });
|
|
445
563
|
mockSocket._triggerOpen();
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('should queue new-entity messages', () => {
|
|
449
|
-
// Clear the initial init message
|
|
450
564
|
mockSocket.send.mockClear();
|
|
565
|
+
});
|
|
451
566
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
567
|
+
it('should queue and replay timeseries messages', () => {
|
|
568
|
+
const msg: TimeSeriesMetricsMessage = {
|
|
569
|
+
event: 'timeseries', seq: 1, data: [
|
|
570
|
+
{ subject: 's', type: 't', timestamp: [0, 0], value: 42, unit: 'By', attributes: {} }
|
|
571
|
+
]
|
|
457
572
|
};
|
|
458
|
-
|
|
459
|
-
connection.sendMessage(serviceMessage);
|
|
460
|
-
|
|
461
|
-
// Should be queued, not sent immediately (not authorized)
|
|
573
|
+
connection.sendMessage(msg);
|
|
462
574
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
463
575
|
|
|
464
|
-
|
|
465
|
-
mockSocket.
|
|
466
|
-
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
576
|
+
mockSocket._triggerMessage(authAck());
|
|
577
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
|
|
467
578
|
});
|
|
468
579
|
|
|
469
|
-
it('should queue
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const httpRequestMessage = {
|
|
474
|
-
event: 'new-interaction' as const,
|
|
475
|
-
type: 'http-request' as const,
|
|
476
|
-
hash: 'request-hash',
|
|
477
|
-
from: 'service-hash',
|
|
478
|
-
to: 'http-service-hash'
|
|
580
|
+
it('should queue and replay cumulative messages', () => {
|
|
581
|
+
const msg: CumulativeMetricsMessage = {
|
|
582
|
+
event: 'cumulative', seq: 2, data: []
|
|
479
583
|
};
|
|
480
|
-
|
|
481
|
-
connection.sendMessage(httpRequestMessage);
|
|
482
|
-
|
|
483
|
-
// Should be queued
|
|
584
|
+
connection.sendMessage(msg);
|
|
484
585
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
485
586
|
|
|
486
|
-
|
|
487
|
-
mockSocket.
|
|
488
|
-
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(httpRequestMessage));
|
|
587
|
+
mockSocket._triggerMessage(authAck());
|
|
588
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
|
|
489
589
|
});
|
|
490
590
|
|
|
491
|
-
it('should queue
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
event: 'observations',
|
|
497
|
-
seq: 1,
|
|
498
|
-
observations: [{
|
|
499
|
-
type: 'http-server',
|
|
500
|
-
subject: 'route-hash',
|
|
501
|
-
timestamp: [1700000000, 0],
|
|
502
|
-
path: '/test',
|
|
503
|
-
status: 200,
|
|
504
|
-
duration: [0, 50000000]
|
|
505
|
-
} as HttpServerObservation]
|
|
591
|
+
it('should queue and replay tracespans messages', () => {
|
|
592
|
+
const msg: TraceSpansMessage = {
|
|
593
|
+
event: 'tracespans', seq: 3, data: [
|
|
594
|
+
{ trace: 't1', span: 's1', parent: '', name: 'test', timestamp: [0, 0] }
|
|
595
|
+
]
|
|
506
596
|
};
|
|
507
|
-
|
|
508
|
-
connection.sendMessage(observationMessage);
|
|
509
|
-
|
|
510
|
-
// Should be queued
|
|
597
|
+
connection.sendMessage(msg);
|
|
511
598
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
512
599
|
|
|
513
|
-
|
|
514
|
-
mockSocket.
|
|
515
|
-
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
|
|
600
|
+
mockSocket._triggerMessage(authAck());
|
|
601
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
|
|
516
602
|
});
|
|
517
603
|
|
|
518
|
-
it('should
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const initMessage: InitMessage = {
|
|
523
|
-
event: 'init',
|
|
524
|
-
protocolVersion: 1,
|
|
525
|
-
token: 'test-token'
|
|
604
|
+
it('should queue and replay db-query messages', () => {
|
|
605
|
+
const msg: DatabaseQueryMessage = {
|
|
606
|
+
event: 'db-query', seq: 4, query: 'SELECT 1', from: 'a', to: 'b',
|
|
607
|
+
timestamp: [0, 0], duration: [0, 1000000]
|
|
526
608
|
};
|
|
609
|
+
connection.sendMessage(msg);
|
|
610
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
527
611
|
|
|
528
|
-
|
|
529
|
-
mockSocket.
|
|
530
|
-
(connection as any).sendRaw(initMessage);
|
|
531
|
-
|
|
532
|
-
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(initMessage));
|
|
612
|
+
mockSocket._triggerMessage(authAck());
|
|
613
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(msg));
|
|
533
614
|
});
|
|
534
615
|
});
|
|
535
|
-
});
|
|
616
|
+
});
|