@comprehend/telemetry-node 0.1.2 → 0.1.4
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 -2
- package/.idea/telemetry-node.iml +1 -0
- package/DEVELOPMENT.md +69 -0
- package/README.md +127 -0
- package/dist/ComprehendDevSpanProcessor.js +1 -0
- package/dist/ComprehendDevSpanProcessor.test.d.ts +1 -0
- package/dist/ComprehendDevSpanProcessor.test.js +674 -0
- package/dist/WebSocketConnection.test.d.ts +1 -0
- package/dist/WebSocketConnection.test.js +406 -0
- package/dist/sql-analyzer.js +11 -2
- package/dist/sql-analyzer.test.js +12 -0
- package/jest.config.js +1 -0
- package/package.json +2 -2
- package/src/ComprehendDevSpanProcessor.test.ts +822 -0
- package/src/ComprehendDevSpanProcessor.ts +1 -0
- package/src/WebSocketConnection.test.ts +535 -0
- package/src/sql-analyzer.test.ts +16 -0
- package/src/sql-analyzer.ts +11 -2
|
@@ -0,0 +1,406 @@
|
|
|
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
|
+
const ws_1 = __importDefault(require("ws"));
|
|
10
|
+
const MockedWebSocket = ws_1.default;
|
|
11
|
+
describe('WebSocketConnection', () => {
|
|
12
|
+
let mockSocket;
|
|
13
|
+
let connection;
|
|
14
|
+
let logMessages;
|
|
15
|
+
let mockLogger;
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset all mocks
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
jest.clearAllTimers();
|
|
20
|
+
jest.useFakeTimers();
|
|
21
|
+
logMessages = [];
|
|
22
|
+
mockLogger = jest.fn((message) => {
|
|
23
|
+
logMessages.push(message);
|
|
24
|
+
});
|
|
25
|
+
// Create mock WebSocket instance with event handling
|
|
26
|
+
mockSocket = {
|
|
27
|
+
on: jest.fn(),
|
|
28
|
+
send: jest.fn(),
|
|
29
|
+
close: jest.fn(),
|
|
30
|
+
readyState: ws_1.default.CLOSED,
|
|
31
|
+
OPEN: ws_1.default.OPEN,
|
|
32
|
+
CLOSED: ws_1.default.CLOSED,
|
|
33
|
+
_triggerOpen: function () { },
|
|
34
|
+
_triggerMessage: function (data) { },
|
|
35
|
+
_triggerClose: function (code, reason) { },
|
|
36
|
+
_triggerError: function (error) { }
|
|
37
|
+
};
|
|
38
|
+
// Set up event handler storage and triggering
|
|
39
|
+
const eventHandlers = {};
|
|
40
|
+
mockSocket.on.mockImplementation((event, handler) => {
|
|
41
|
+
eventHandlers[event] = handler;
|
|
42
|
+
});
|
|
43
|
+
mockSocket._triggerOpen = () => {
|
|
44
|
+
mockSocket.readyState = ws_1.default.OPEN;
|
|
45
|
+
eventHandlers['open']?.();
|
|
46
|
+
};
|
|
47
|
+
mockSocket._triggerMessage = (data) => {
|
|
48
|
+
const buffer = Buffer.from(data);
|
|
49
|
+
eventHandlers['message']?.(buffer);
|
|
50
|
+
};
|
|
51
|
+
mockSocket._triggerClose = (code, reason) => {
|
|
52
|
+
mockSocket.readyState = ws_1.default.CLOSED;
|
|
53
|
+
eventHandlers['close']?.(code, Buffer.from(reason));
|
|
54
|
+
};
|
|
55
|
+
mockSocket._triggerError = (error) => {
|
|
56
|
+
eventHandlers['error']?.(error);
|
|
57
|
+
};
|
|
58
|
+
// Mock WebSocket constructor to return our mock instance
|
|
59
|
+
MockedWebSocket.mockImplementation(() => mockSocket);
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
jest.useRealTimers();
|
|
63
|
+
if (connection) {
|
|
64
|
+
connection.close();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
describe('Connection Establishment', () => {
|
|
68
|
+
it('should create WebSocket connection with correct URL', () => {
|
|
69
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
70
|
+
expect(MockedWebSocket).toHaveBeenCalledWith('wss://ingestion.comprehend.dev/test-org/observations');
|
|
71
|
+
expect(mockSocket.on).toHaveBeenCalledWith('open', expect.any(Function));
|
|
72
|
+
expect(mockSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
73
|
+
expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
74
|
+
expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
75
|
+
});
|
|
76
|
+
it('should send init message on connection open', () => {
|
|
77
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
78
|
+
mockSocket._triggerOpen();
|
|
79
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify({
|
|
80
|
+
event: 'init',
|
|
81
|
+
protocolVersion: 1,
|
|
82
|
+
token: 'test-token'
|
|
83
|
+
}));
|
|
84
|
+
expect(logMessages).toContain('WebSocket connected. Sending init/auth message.');
|
|
85
|
+
});
|
|
86
|
+
it('should work without logger', () => {
|
|
87
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token');
|
|
88
|
+
mockSocket._triggerOpen();
|
|
89
|
+
expect(MockedWebSocket).toHaveBeenCalled();
|
|
90
|
+
expect(mockSocket.send).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('Authorization Flow', () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
96
|
+
mockSocket._triggerOpen();
|
|
97
|
+
jest.clearAllMocks(); // Clear the init message send
|
|
98
|
+
});
|
|
99
|
+
it('should handle authorization acknowledgment', () => {
|
|
100
|
+
const authAck = {
|
|
101
|
+
type: 'ack-authorized'
|
|
102
|
+
};
|
|
103
|
+
mockSocket._triggerMessage(JSON.stringify(authAck));
|
|
104
|
+
expect(logMessages).toContain('Authorization acknowledged by server.');
|
|
105
|
+
});
|
|
106
|
+
it('should replay queued messages after authorization', () => {
|
|
107
|
+
// Queue some messages before authorization
|
|
108
|
+
const serviceMessage = {
|
|
109
|
+
event: 'new-entity',
|
|
110
|
+
type: 'service',
|
|
111
|
+
hash: 'test-hash-1',
|
|
112
|
+
name: 'test-service'
|
|
113
|
+
};
|
|
114
|
+
const observationMessage = {
|
|
115
|
+
event: 'observations',
|
|
116
|
+
seq: 1,
|
|
117
|
+
observations: [{
|
|
118
|
+
type: 'http-server',
|
|
119
|
+
subject: 'test-subject',
|
|
120
|
+
timestamp: [1700000000, 0],
|
|
121
|
+
path: '/test',
|
|
122
|
+
status: 200,
|
|
123
|
+
duration: [0, 100000000]
|
|
124
|
+
}]
|
|
125
|
+
};
|
|
126
|
+
connection.sendMessage(serviceMessage);
|
|
127
|
+
connection.sendMessage(observationMessage);
|
|
128
|
+
// Should not send immediately (not authorized yet)
|
|
129
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
130
|
+
// Now authorize
|
|
131
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
132
|
+
// Should replay both messages
|
|
133
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(2);
|
|
134
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
135
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
|
|
136
|
+
});
|
|
137
|
+
it('should send messages immediately when already authorized', () => {
|
|
138
|
+
// First authorize
|
|
139
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
140
|
+
jest.clearAllMocks();
|
|
141
|
+
const serviceMessage = {
|
|
142
|
+
event: 'new-entity',
|
|
143
|
+
type: 'service',
|
|
144
|
+
hash: 'test-hash',
|
|
145
|
+
name: 'test-service'
|
|
146
|
+
};
|
|
147
|
+
connection.sendMessage(serviceMessage);
|
|
148
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('Message Acknowledgment Handling', () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
154
|
+
mockSocket._triggerOpen();
|
|
155
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
156
|
+
jest.clearAllMocks();
|
|
157
|
+
});
|
|
158
|
+
it('should handle entity/interaction acknowledgments', () => {
|
|
159
|
+
const serviceMessage = {
|
|
160
|
+
event: 'new-entity',
|
|
161
|
+
type: 'service',
|
|
162
|
+
hash: 'service-hash',
|
|
163
|
+
name: 'test-service'
|
|
164
|
+
};
|
|
165
|
+
const routeMessage = {
|
|
166
|
+
event: 'new-entity',
|
|
167
|
+
type: 'http-route',
|
|
168
|
+
hash: 'route-hash',
|
|
169
|
+
parent: 'service-hash',
|
|
170
|
+
method: 'GET',
|
|
171
|
+
route: '/test'
|
|
172
|
+
};
|
|
173
|
+
// Send messages
|
|
174
|
+
connection.sendMessage(serviceMessage);
|
|
175
|
+
connection.sendMessage(routeMessage);
|
|
176
|
+
// Acknowledge the service message
|
|
177
|
+
mockSocket._triggerMessage(JSON.stringify({
|
|
178
|
+
type: 'ack-observed',
|
|
179
|
+
hash: 'service-hash'
|
|
180
|
+
}));
|
|
181
|
+
// Clear previous calls before reconnection test
|
|
182
|
+
mockSocket.send.mockClear();
|
|
183
|
+
// If we reconnect, only the route message should be replayed
|
|
184
|
+
mockSocket._triggerClose(1000, 'test close');
|
|
185
|
+
jest.advanceTimersByTime(1000);
|
|
186
|
+
mockSocket._triggerOpen();
|
|
187
|
+
// Clear the init message that gets sent on reconnection
|
|
188
|
+
mockSocket.send.mockClear();
|
|
189
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
190
|
+
// Should only send the unacknowledged route message
|
|
191
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(1);
|
|
192
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(routeMessage));
|
|
193
|
+
});
|
|
194
|
+
it('should handle observation acknowledgments', () => {
|
|
195
|
+
const observationMessage1 = {
|
|
196
|
+
event: 'observations',
|
|
197
|
+
seq: 1,
|
|
198
|
+
observations: []
|
|
199
|
+
};
|
|
200
|
+
const observationMessage2 = {
|
|
201
|
+
event: 'observations',
|
|
202
|
+
seq: 2,
|
|
203
|
+
observations: []
|
|
204
|
+
};
|
|
205
|
+
// Send messages
|
|
206
|
+
connection.sendMessage(observationMessage1);
|
|
207
|
+
connection.sendMessage(observationMessage2);
|
|
208
|
+
// Acknowledge first observation
|
|
209
|
+
mockSocket._triggerMessage(JSON.stringify({
|
|
210
|
+
type: 'ack-observations',
|
|
211
|
+
seq: 1
|
|
212
|
+
}));
|
|
213
|
+
// Clear previous calls before reconnection test
|
|
214
|
+
mockSocket.send.mockClear();
|
|
215
|
+
// Reconnect - only second observation should be replayed
|
|
216
|
+
mockSocket._triggerClose(1000, 'test close');
|
|
217
|
+
jest.advanceTimersByTime(1000);
|
|
218
|
+
mockSocket._triggerOpen();
|
|
219
|
+
// Clear the init message that gets sent on reconnection
|
|
220
|
+
mockSocket.send.mockClear();
|
|
221
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
222
|
+
// Should only send the unacknowledged second observation message
|
|
223
|
+
expect(mockSocket.send).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage2));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe('Reconnection Logic', () => {
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
230
|
+
mockSocket._triggerOpen();
|
|
231
|
+
});
|
|
232
|
+
it('should reconnect automatically on close', () => {
|
|
233
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
234
|
+
mockSocket._triggerClose(1000, 'Normal closure');
|
|
235
|
+
expect(logMessages).toContain('WebSocket disconnected. Code: 1000, Reason: Normal closure');
|
|
236
|
+
// Should schedule reconnection
|
|
237
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 1000);
|
|
238
|
+
// Advance timer to trigger reconnection
|
|
239
|
+
jest.advanceTimersByTime(1000);
|
|
240
|
+
// Should create new WebSocket connection
|
|
241
|
+
expect(MockedWebSocket).toHaveBeenCalledTimes(2);
|
|
242
|
+
setTimeoutSpy.mockRestore();
|
|
243
|
+
});
|
|
244
|
+
it('should reset authorization state on close', () => {
|
|
245
|
+
// First authorize
|
|
246
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
247
|
+
// Send a message (should send immediately)
|
|
248
|
+
const testMessage = {
|
|
249
|
+
event: 'new-entity',
|
|
250
|
+
type: 'service',
|
|
251
|
+
hash: 'test-hash',
|
|
252
|
+
name: 'test-service'
|
|
253
|
+
};
|
|
254
|
+
connection.sendMessage(testMessage);
|
|
255
|
+
expect(mockSocket.send).toHaveBeenCalled();
|
|
256
|
+
jest.clearAllMocks();
|
|
257
|
+
// Close connection
|
|
258
|
+
mockSocket._triggerClose(1000, 'test');
|
|
259
|
+
// Send another message - should queue (not send immediately)
|
|
260
|
+
connection.sendMessage(testMessage);
|
|
261
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
it('should not reconnect when explicitly closed', () => {
|
|
264
|
+
connection.close();
|
|
265
|
+
mockSocket._triggerClose(1000, 'Explicit close');
|
|
266
|
+
// Should not schedule reconnection
|
|
267
|
+
jest.advanceTimersByTime(5000);
|
|
268
|
+
expect(MockedWebSocket).toHaveBeenCalledTimes(1); // Only initial connection
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('Error Handling', () => {
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
274
|
+
});
|
|
275
|
+
it('should log WebSocket errors', () => {
|
|
276
|
+
const testError = new Error('Connection failed');
|
|
277
|
+
mockSocket._triggerError(testError);
|
|
278
|
+
expect(logMessages).toContain('WebSocket encountered an error: Connection failed');
|
|
279
|
+
});
|
|
280
|
+
it('should handle malformed server messages gracefully', () => {
|
|
281
|
+
mockSocket._triggerOpen();
|
|
282
|
+
mockSocket._triggerMessage('{invalid json}');
|
|
283
|
+
// Check that an error message was logged (exact message varies by Node version)
|
|
284
|
+
const errorLogExists = logMessages.some(msg => msg.startsWith('Error parsing message from server:') &&
|
|
285
|
+
msg.includes('JSON'));
|
|
286
|
+
expect(errorLogExists).toBe(true);
|
|
287
|
+
});
|
|
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
|
+
it('should not send messages when socket is not open', () => {
|
|
301
|
+
mockSocket.readyState = ws_1.default.CLOSED;
|
|
302
|
+
mockSocket._triggerOpen();
|
|
303
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
304
|
+
// Clear the init and any previous messages
|
|
305
|
+
mockSocket.send.mockClear();
|
|
306
|
+
// Force socket to closed state
|
|
307
|
+
mockSocket.readyState = ws_1.default.CLOSED;
|
|
308
|
+
const testMessage = {
|
|
309
|
+
event: 'new-entity',
|
|
310
|
+
type: 'service',
|
|
311
|
+
hash: 'test-hash',
|
|
312
|
+
name: 'test-service'
|
|
313
|
+
};
|
|
314
|
+
connection.sendMessage(testMessage);
|
|
315
|
+
// Should not call send when socket is closed
|
|
316
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('Connection Lifecycle', () => {
|
|
320
|
+
it('should close WebSocket connection properly', () => {
|
|
321
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
322
|
+
connection.close();
|
|
323
|
+
expect(mockSocket.close).toHaveBeenCalled();
|
|
324
|
+
});
|
|
325
|
+
it('should handle close when socket is null', () => {
|
|
326
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
327
|
+
// Simulate socket being null
|
|
328
|
+
connection.socket = null;
|
|
329
|
+
expect(() => connection.close()).not.toThrow();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
describe('Message Types and Queuing', () => {
|
|
333
|
+
beforeEach(() => {
|
|
334
|
+
connection = new WebSocketConnection_1.WebSocketConnection('test-org', 'test-token', mockLogger);
|
|
335
|
+
mockSocket._triggerOpen();
|
|
336
|
+
});
|
|
337
|
+
it('should queue new-entity messages', () => {
|
|
338
|
+
// Clear the initial init message
|
|
339
|
+
mockSocket.send.mockClear();
|
|
340
|
+
const serviceMessage = {
|
|
341
|
+
event: 'new-entity',
|
|
342
|
+
type: 'service',
|
|
343
|
+
hash: 'service-hash',
|
|
344
|
+
name: 'test-service'
|
|
345
|
+
};
|
|
346
|
+
connection.sendMessage(serviceMessage);
|
|
347
|
+
// Should be queued, not sent immediately (not authorized)
|
|
348
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
349
|
+
// Authorize and check replay
|
|
350
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
351
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(serviceMessage));
|
|
352
|
+
});
|
|
353
|
+
it('should queue new-interaction messages', () => {
|
|
354
|
+
// Clear the initial init message
|
|
355
|
+
mockSocket.send.mockClear();
|
|
356
|
+
const httpRequestMessage = {
|
|
357
|
+
event: 'new-interaction',
|
|
358
|
+
type: 'http-request',
|
|
359
|
+
hash: 'request-hash',
|
|
360
|
+
from: 'service-hash',
|
|
361
|
+
to: 'http-service-hash'
|
|
362
|
+
};
|
|
363
|
+
connection.sendMessage(httpRequestMessage);
|
|
364
|
+
// Should be queued
|
|
365
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
366
|
+
// Authorize and check replay
|
|
367
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
368
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(httpRequestMessage));
|
|
369
|
+
});
|
|
370
|
+
it('should queue observation messages', () => {
|
|
371
|
+
// Clear the initial init message
|
|
372
|
+
mockSocket.send.mockClear();
|
|
373
|
+
const observationMessage = {
|
|
374
|
+
event: 'observations',
|
|
375
|
+
seq: 1,
|
|
376
|
+
observations: [{
|
|
377
|
+
type: 'http-server',
|
|
378
|
+
subject: 'route-hash',
|
|
379
|
+
timestamp: [1700000000, 0],
|
|
380
|
+
path: '/test',
|
|
381
|
+
status: 200,
|
|
382
|
+
duration: [0, 50000000]
|
|
383
|
+
}]
|
|
384
|
+
};
|
|
385
|
+
connection.sendMessage(observationMessage);
|
|
386
|
+
// Should be queued
|
|
387
|
+
expect(mockSocket.send).not.toHaveBeenCalled();
|
|
388
|
+
// Authorize and check replay
|
|
389
|
+
mockSocket._triggerMessage(JSON.stringify({ type: 'ack-authorized' }));
|
|
390
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(observationMessage));
|
|
391
|
+
});
|
|
392
|
+
it('should not queue init messages in unacknowledged maps', () => {
|
|
393
|
+
// This is implicit - init messages are sent via sendRaw, not sendMessage
|
|
394
|
+
// and don't go through the queuing logic
|
|
395
|
+
const initMessage = {
|
|
396
|
+
event: 'init',
|
|
397
|
+
protocolVersion: 1,
|
|
398
|
+
token: 'test-token'
|
|
399
|
+
};
|
|
400
|
+
// Manually test sendRaw behavior (private method testing)
|
|
401
|
+
mockSocket.readyState = ws_1.default.OPEN;
|
|
402
|
+
connection.sendRaw(initMessage);
|
|
403
|
+
expect(mockSocket.send).toHaveBeenCalledWith(JSON.stringify(initMessage));
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
package/dist/sql-analyzer.js
CHANGED
|
@@ -23,6 +23,7 @@ function analyzeSQL(sql) {
|
|
|
23
23
|
let skippingValues = false;
|
|
24
24
|
let lookingForCommaOrEnd = false;
|
|
25
25
|
let valuesDepth = 0;
|
|
26
|
+
let skippedWhitespace = [];
|
|
26
27
|
for (let token of tokenizeSQL(sql)) {
|
|
27
28
|
switch (token.type) {
|
|
28
29
|
case "whitespace":
|
|
@@ -136,23 +137,31 @@ function analyzeSQL(sql) {
|
|
|
136
137
|
switch (token.type) {
|
|
137
138
|
case "comment":
|
|
138
139
|
case "whitespace":
|
|
139
|
-
//
|
|
140
|
+
// Collect whitespace/comments while looking for comma or end
|
|
141
|
+
skippedWhitespace.push(token);
|
|
140
142
|
break;
|
|
141
143
|
case "punct":
|
|
142
144
|
if (token.value === ",") {
|
|
143
|
-
// More tuples coming, continue skipping
|
|
145
|
+
// More tuples coming, clear skipped whitespace and continue skipping
|
|
146
|
+
skippedWhitespace = [];
|
|
144
147
|
lookingForCommaOrEnd = false;
|
|
145
148
|
skippingValues = true;
|
|
146
149
|
}
|
|
147
150
|
else {
|
|
148
151
|
// Not a comma, so VALUES clause is done
|
|
152
|
+
// Add back the skipped whitespace, then the current token
|
|
153
|
+
presentableTokens.push(...skippedWhitespace);
|
|
149
154
|
presentableTokens.push(token);
|
|
155
|
+
skippedWhitespace = [];
|
|
150
156
|
lookingForCommaOrEnd = false;
|
|
151
157
|
}
|
|
152
158
|
break;
|
|
153
159
|
default:
|
|
154
160
|
// VALUES clause is done, resume normal processing
|
|
161
|
+
// Add back the skipped whitespace, then the current token
|
|
162
|
+
presentableTokens.push(...skippedWhitespace);
|
|
155
163
|
presentableTokens.push(token);
|
|
164
|
+
skippedWhitespace = [];
|
|
156
165
|
lookingForCommaOrEnd = false;
|
|
157
166
|
break;
|
|
158
167
|
}
|
|
@@ -482,4 +482,16 @@ 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
|
+
});
|
|
485
497
|
});
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comprehend/telemetry-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Integration of comprehend.dev with OpenTelemetry in Node.js and similar environemnts.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"test": "jest"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [],
|
|
13
|
-
"author": "Comprehend AB",
|
|
13
|
+
"author": "Comprehend.dev AB",
|
|
14
14
|
"license": "LicenseRef-Proprietary-Audit",
|
|
15
15
|
"private": false,
|
|
16
16
|
"dependencies": {
|