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