@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
|
@@ -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:
|
|
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();
|
|
100
|
+
jest.clearAllMocks();
|
|
98
101
|
});
|
|
99
|
-
it('should handle authorization acknowledgment', () => {
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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(
|
|
222
|
-
|
|
223
|
-
expect(
|
|
224
|
-
|
|
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
|
-
|
|
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);
|
|
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(
|
|
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
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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(
|
|
347
|
-
// Should be queued, not sent immediately (not authorized)
|
|
437
|
+
connection.sendMessage(msg);
|
|
348
438
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
349
|
-
|
|
350
|
-
mockSocket.
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
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(
|
|
364
|
-
// Should be queued
|
|
446
|
+
connection.sendMessage(msg);
|
|
365
447
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
366
|
-
|
|
367
|
-
mockSocket.
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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(
|
|
386
|
-
// Should be queued
|
|
457
|
+
connection.sendMessage(msg);
|
|
387
458
|
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
388
|
-
|
|
389
|
-
mockSocket.
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
401
|
-
mockSocket.
|
|
402
|
-
|
|
403
|
-
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(
|
|
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 {
|
|
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; } });
|
package/dist/util.d.ts
ADDED