trickle-observe 0.2.97 → 0.2.99
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/dist/observe-register.js +27 -6
- package/dist/ws-observer.d.ts +21 -0
- package/dist/ws-observer.js +220 -0
- package/package.json +1 -1
- package/src/observe-register.ts +27 -6
- package/src/ws-observer.ts +190 -0
package/dist/observe-register.js
CHANGED
|
@@ -757,7 +757,10 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
757
757
|
}
|
|
758
758
|
}
|
|
759
759
|
// Also find variable declarations for tracing
|
|
760
|
-
|
|
760
|
+
// In production mode, disable variable tracing by default
|
|
761
|
+
const isProduction = process.env.TRICKLE_PRODUCTION === '1' || process.env.TRICKLE_PRODUCTION === 'true';
|
|
762
|
+
const varTraceDefault = isProduction ? '0' : '1';
|
|
763
|
+
const varTraceEnabled = (process.env.TRICKLE_TRACE_VARS || varTraceDefault) !== '0';
|
|
761
764
|
// For TypeScript files (compiled by ts-node/tsc), type declarations (interfaces, type aliases)
|
|
762
765
|
// are stripped from the compiled JS, shifting line numbers. The only accurate way to get correct
|
|
763
766
|
// line numbers is to read the original .ts source file and parse it directly.
|
|
@@ -885,7 +888,7 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
885
888
|
` module: ${JSON.stringify(effectiveModuleName)},`,
|
|
886
889
|
` trackArgs: true,`,
|
|
887
890
|
` trackReturn: true,`,
|
|
888
|
-
` sampleRate: 1,`,
|
|
891
|
+
` sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),`,
|
|
889
892
|
` maxDepth: 3,`,
|
|
890
893
|
` environment: ${JSON.stringify(env)},`,
|
|
891
894
|
` enabled: true,`,
|
|
@@ -1166,6 +1169,24 @@ if (enabled) {
|
|
|
1166
1169
|
}
|
|
1167
1170
|
catch { /* not critical */ }
|
|
1168
1171
|
}
|
|
1172
|
+
// WebSocket (ws)
|
|
1173
|
+
if (request === 'ws' && !expressPatched.has('ws')) {
|
|
1174
|
+
expressPatched.add('ws');
|
|
1175
|
+
try {
|
|
1176
|
+
const { patchWs } = require(path_1.default.join(__dirname, 'ws-observer.js'));
|
|
1177
|
+
patchWs(exports, debug);
|
|
1178
|
+
}
|
|
1179
|
+
catch { /* not critical */ }
|
|
1180
|
+
}
|
|
1181
|
+
// socket.io-client
|
|
1182
|
+
if (request === 'socket.io-client' && !expressPatched.has('socket.io-client')) {
|
|
1183
|
+
expressPatched.add('socket.io-client');
|
|
1184
|
+
try {
|
|
1185
|
+
const { patchSocketIo } = require(path_1.default.join(__dirname, 'ws-observer.js'));
|
|
1186
|
+
patchSocketIo(exports, debug);
|
|
1187
|
+
}
|
|
1188
|
+
catch { /* not critical */ }
|
|
1189
|
+
}
|
|
1169
1190
|
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
1170
1191
|
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
1171
1192
|
// with './' or '/'. We need the resolved path to decide if it's user code.
|
|
@@ -1235,7 +1256,7 @@ if (enabled) {
|
|
|
1235
1256
|
module: moduleName,
|
|
1236
1257
|
trackArgs: true,
|
|
1237
1258
|
trackReturn: true,
|
|
1238
|
-
sampleRate: 1,
|
|
1259
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1239
1260
|
maxDepth: 3,
|
|
1240
1261
|
environment,
|
|
1241
1262
|
enabled: true,
|
|
@@ -1288,7 +1309,7 @@ if (enabled) {
|
|
|
1288
1309
|
module: moduleName,
|
|
1289
1310
|
trackArgs: true,
|
|
1290
1311
|
trackReturn: true,
|
|
1291
|
-
sampleRate: 1,
|
|
1312
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1292
1313
|
maxDepth: 3,
|
|
1293
1314
|
environment,
|
|
1294
1315
|
enabled: true,
|
|
@@ -1341,7 +1362,7 @@ if (enabled) {
|
|
|
1341
1362
|
module: moduleName,
|
|
1342
1363
|
trackArgs: true,
|
|
1343
1364
|
trackReturn: true,
|
|
1344
|
-
sampleRate: 1,
|
|
1365
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1345
1366
|
maxDepth: 3,
|
|
1346
1367
|
environment,
|
|
1347
1368
|
enabled: true,
|
|
@@ -1374,7 +1395,7 @@ if (enabled) {
|
|
|
1374
1395
|
module: moduleName,
|
|
1375
1396
|
trackArgs: true,
|
|
1376
1397
|
trackReturn: true,
|
|
1377
|
-
sampleRate: 1,
|
|
1398
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1378
1399
|
maxDepth: 3,
|
|
1379
1400
|
environment,
|
|
1380
1401
|
enabled: true,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket observer — patches popular WebSocket libraries to capture
|
|
3
|
+
* message events, connection lifecycle, and timing.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - ws (most popular Node.js WebSocket library)
|
|
7
|
+
* - Native WebSocket (browser/Deno/Bun)
|
|
8
|
+
* - socket.io (real-time framework)
|
|
9
|
+
*
|
|
10
|
+
* Written to .trickle/websocket.jsonl as:
|
|
11
|
+
* { "kind": "ws", "event": "message", "direction": "in",
|
|
12
|
+
* "data": "...", "timestamp": 1710516000, "url": "ws://..." }
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Patch ws (node WebSocket library) to capture messages.
|
|
16
|
+
*/
|
|
17
|
+
export declare function patchWs(wsModule: any, debug: boolean): void;
|
|
18
|
+
/**
|
|
19
|
+
* Patch socket.io client to capture emit/on events.
|
|
20
|
+
*/
|
|
21
|
+
export declare function patchSocketIo(ioModule: any, debug: boolean): void;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket observer — patches popular WebSocket libraries to capture
|
|
4
|
+
* message events, connection lifecycle, and timing.
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - ws (most popular Node.js WebSocket library)
|
|
8
|
+
* - Native WebSocket (browser/Deno/Bun)
|
|
9
|
+
* - socket.io (real-time framework)
|
|
10
|
+
*
|
|
11
|
+
* Written to .trickle/websocket.jsonl as:
|
|
12
|
+
* { "kind": "ws", "event": "message", "direction": "in",
|
|
13
|
+
* "data": "...", "timestamp": 1710516000, "url": "ws://..." }
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.patchWs = patchWs;
|
|
50
|
+
exports.patchSocketIo = patchSocketIo;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
let wsFile = null;
|
|
54
|
+
const MAX_EVENTS = 200;
|
|
55
|
+
let eventCount = 0;
|
|
56
|
+
const MAX_DATA_LENGTH = 500;
|
|
57
|
+
function getWsFile() {
|
|
58
|
+
if (wsFile)
|
|
59
|
+
return wsFile;
|
|
60
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
wsFile = path.join(dir, 'websocket.jsonl');
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(wsFile, '');
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
;
|
|
71
|
+
return wsFile;
|
|
72
|
+
}
|
|
73
|
+
function writeWsEvent(event) {
|
|
74
|
+
if (eventCount >= MAX_EVENTS)
|
|
75
|
+
return;
|
|
76
|
+
eventCount++;
|
|
77
|
+
try {
|
|
78
|
+
fs.appendFileSync(getWsFile(), JSON.stringify(event) + '\n');
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
function truncateData(data) {
|
|
83
|
+
if (data === undefined || data === null)
|
|
84
|
+
return data;
|
|
85
|
+
if (typeof data === 'string') {
|
|
86
|
+
return data.length > MAX_DATA_LENGTH ? data.substring(0, MAX_DATA_LENGTH) + '...' : data;
|
|
87
|
+
}
|
|
88
|
+
if (Buffer.isBuffer(data)) {
|
|
89
|
+
return `<Buffer(${data.length} bytes)>`;
|
|
90
|
+
}
|
|
91
|
+
if (typeof data === 'object') {
|
|
92
|
+
try {
|
|
93
|
+
const s = JSON.stringify(data);
|
|
94
|
+
return s.length > MAX_DATA_LENGTH ? JSON.parse(s.substring(0, MAX_DATA_LENGTH) + '..."}}') : data;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return String(data).substring(0, MAX_DATA_LENGTH);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Patch ws (node WebSocket library) to capture messages.
|
|
104
|
+
*/
|
|
105
|
+
function patchWs(wsModule, debug) {
|
|
106
|
+
const WsClass = wsModule.WebSocket || wsModule;
|
|
107
|
+
if (!WsClass?.prototype || WsClass.prototype.__trickle_ws_patched)
|
|
108
|
+
return;
|
|
109
|
+
const origSend = WsClass.prototype.send;
|
|
110
|
+
if (origSend) {
|
|
111
|
+
WsClass.prototype.send = function patchedSend(data, ...args) {
|
|
112
|
+
writeWsEvent({
|
|
113
|
+
kind: 'ws',
|
|
114
|
+
event: 'message',
|
|
115
|
+
direction: 'out',
|
|
116
|
+
url: this.url || this._url,
|
|
117
|
+
data: truncateData(data),
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
return origSend.call(this, data, ...args);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Patch 'on' to capture incoming messages
|
|
124
|
+
const origOn = WsClass.prototype.on;
|
|
125
|
+
if (origOn) {
|
|
126
|
+
WsClass.prototype.on = function patchedOn(event, listener, ...args) {
|
|
127
|
+
if (event === 'message') {
|
|
128
|
+
const wrappedListener = (data, ...rest) => {
|
|
129
|
+
writeWsEvent({
|
|
130
|
+
kind: 'ws',
|
|
131
|
+
event: 'message',
|
|
132
|
+
direction: 'in',
|
|
133
|
+
url: this.url || this._url,
|
|
134
|
+
data: truncateData(data),
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
return listener(data, ...rest);
|
|
138
|
+
};
|
|
139
|
+
return origOn.call(this, event, wrappedListener, ...args);
|
|
140
|
+
}
|
|
141
|
+
if (event === 'open') {
|
|
142
|
+
const wrappedListener = (...rest) => {
|
|
143
|
+
writeWsEvent({
|
|
144
|
+
kind: 'ws',
|
|
145
|
+
event: 'connect',
|
|
146
|
+
url: this.url || this._url,
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
});
|
|
149
|
+
return listener(...rest);
|
|
150
|
+
};
|
|
151
|
+
return origOn.call(this, event, wrappedListener, ...args);
|
|
152
|
+
}
|
|
153
|
+
if (event === 'close') {
|
|
154
|
+
const wrappedListener = (code, reason, ...rest) => {
|
|
155
|
+
writeWsEvent({
|
|
156
|
+
kind: 'ws',
|
|
157
|
+
event: 'close',
|
|
158
|
+
url: this.url || this._url,
|
|
159
|
+
data: { code, reason: String(reason || '').substring(0, 100) },
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
return listener(code, reason, ...rest);
|
|
163
|
+
};
|
|
164
|
+
return origOn.call(this, event, wrappedListener, ...args);
|
|
165
|
+
}
|
|
166
|
+
return origOn.call(this, event, listener, ...args);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
WsClass.prototype.__trickle_ws_patched = true;
|
|
170
|
+
if (debug)
|
|
171
|
+
console.log('[trickle/ws] WebSocket tracing enabled');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Patch socket.io client to capture emit/on events.
|
|
175
|
+
*/
|
|
176
|
+
function patchSocketIo(ioModule, debug) {
|
|
177
|
+
// socket.io-client: the default export is a function that returns a Socket
|
|
178
|
+
const Socket = ioModule.Socket || (ioModule.io && ioModule.io.Socket);
|
|
179
|
+
if (!Socket?.prototype || Socket.prototype.__trickle_sio_patched)
|
|
180
|
+
return;
|
|
181
|
+
const origEmit = Socket.prototype.emit;
|
|
182
|
+
if (origEmit) {
|
|
183
|
+
Socket.prototype.emit = function patchedEmit(event, ...args) {
|
|
184
|
+
if (event !== 'connect' && event !== 'disconnect' && !event.startsWith('__')) {
|
|
185
|
+
writeWsEvent({
|
|
186
|
+
kind: 'ws',
|
|
187
|
+
event: 'emit',
|
|
188
|
+
direction: 'out',
|
|
189
|
+
channel: event,
|
|
190
|
+
data: truncateData(args[0]),
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return origEmit.call(this, event, ...args);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const origOn = Socket.prototype.on;
|
|
198
|
+
if (origOn) {
|
|
199
|
+
Socket.prototype.on = function patchedOn(event, listener, ...rest) {
|
|
200
|
+
if (event !== 'connect' && event !== 'disconnect' && !event.startsWith('__')) {
|
|
201
|
+
const wrappedListener = (...args) => {
|
|
202
|
+
writeWsEvent({
|
|
203
|
+
kind: 'ws',
|
|
204
|
+
event: 'emit',
|
|
205
|
+
direction: 'in',
|
|
206
|
+
channel: event,
|
|
207
|
+
data: truncateData(args[0]),
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
});
|
|
210
|
+
return listener(...args);
|
|
211
|
+
};
|
|
212
|
+
return origOn.call(this, event, wrappedListener, ...rest);
|
|
213
|
+
}
|
|
214
|
+
return origOn.call(this, event, listener, ...rest);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
Socket.prototype.__trickle_sio_patched = true;
|
|
218
|
+
if (debug)
|
|
219
|
+
console.log('[trickle/ws] socket.io tracing enabled');
|
|
220
|
+
}
|
package/package.json
CHANGED
package/src/observe-register.ts
CHANGED
|
@@ -717,7 +717,10 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
717
717
|
}
|
|
718
718
|
|
|
719
719
|
// Also find variable declarations for tracing
|
|
720
|
-
|
|
720
|
+
// In production mode, disable variable tracing by default
|
|
721
|
+
const isProduction = process.env.TRICKLE_PRODUCTION === '1' || process.env.TRICKLE_PRODUCTION === 'true';
|
|
722
|
+
const varTraceDefault = isProduction ? '0' : '1';
|
|
723
|
+
const varTraceEnabled = (process.env.TRICKLE_TRACE_VARS || varTraceDefault) !== '0';
|
|
721
724
|
|
|
722
725
|
// For TypeScript files (compiled by ts-node/tsc), type declarations (interfaces, type aliases)
|
|
723
726
|
// are stripped from the compiled JS, shifting line numbers. The only accurate way to get correct
|
|
@@ -851,7 +854,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
851
854
|
` module: ${JSON.stringify(effectiveModuleName)},`,
|
|
852
855
|
` trackArgs: true,`,
|
|
853
856
|
` trackReturn: true,`,
|
|
854
|
-
` sampleRate: 1,`,
|
|
857
|
+
` sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),`,
|
|
855
858
|
` maxDepth: 3,`,
|
|
856
859
|
` environment: ${JSON.stringify(env)},`,
|
|
857
860
|
` enabled: true,`,
|
|
@@ -1154,6 +1157,24 @@ if (enabled) {
|
|
|
1154
1157
|
} catch { /* not critical */ }
|
|
1155
1158
|
}
|
|
1156
1159
|
|
|
1160
|
+
// WebSocket (ws)
|
|
1161
|
+
if (request === 'ws' && !expressPatched.has('ws')) {
|
|
1162
|
+
expressPatched.add('ws');
|
|
1163
|
+
try {
|
|
1164
|
+
const { patchWs } = require(path.join(__dirname, 'ws-observer.js'));
|
|
1165
|
+
patchWs(exports, debug);
|
|
1166
|
+
} catch { /* not critical */ }
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// socket.io-client
|
|
1170
|
+
if (request === 'socket.io-client' && !expressPatched.has('socket.io-client')) {
|
|
1171
|
+
expressPatched.add('socket.io-client');
|
|
1172
|
+
try {
|
|
1173
|
+
const { patchSocketIo } = require(path.join(__dirname, 'ws-observer.js'));
|
|
1174
|
+
patchSocketIo(exports, debug);
|
|
1175
|
+
} catch { /* not critical */ }
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1157
1178
|
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
1158
1179
|
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
1159
1180
|
// with './' or '/'. We need the resolved path to decide if it's user code.
|
|
@@ -1217,7 +1238,7 @@ if (enabled) {
|
|
|
1217
1238
|
module: moduleName,
|
|
1218
1239
|
trackArgs: true,
|
|
1219
1240
|
trackReturn: true,
|
|
1220
|
-
sampleRate: 1,
|
|
1241
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1221
1242
|
maxDepth: 3,
|
|
1222
1243
|
environment,
|
|
1223
1244
|
enabled: true,
|
|
@@ -1262,7 +1283,7 @@ if (enabled) {
|
|
|
1262
1283
|
module: moduleName,
|
|
1263
1284
|
trackArgs: true,
|
|
1264
1285
|
trackReturn: true,
|
|
1265
|
-
sampleRate: 1,
|
|
1286
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1266
1287
|
maxDepth: 3,
|
|
1267
1288
|
environment,
|
|
1268
1289
|
enabled: true,
|
|
@@ -1304,7 +1325,7 @@ if (enabled) {
|
|
|
1304
1325
|
module: moduleName,
|
|
1305
1326
|
trackArgs: true,
|
|
1306
1327
|
trackReturn: true,
|
|
1307
|
-
sampleRate: 1,
|
|
1328
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1308
1329
|
maxDepth: 3,
|
|
1309
1330
|
environment,
|
|
1310
1331
|
enabled: true,
|
|
@@ -1335,7 +1356,7 @@ if (enabled) {
|
|
|
1335
1356
|
module: moduleName,
|
|
1336
1357
|
trackArgs: true,
|
|
1337
1358
|
trackReturn: true,
|
|
1338
|
-
sampleRate: 1,
|
|
1359
|
+
sampleRate: parseFloat(process.env.TRICKLE_SAMPLE_RATE || '1'),
|
|
1339
1360
|
maxDepth: 3,
|
|
1340
1361
|
environment,
|
|
1341
1362
|
enabled: true,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket observer — patches popular WebSocket libraries to capture
|
|
3
|
+
* message events, connection lifecycle, and timing.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - ws (most popular Node.js WebSocket library)
|
|
7
|
+
* - Native WebSocket (browser/Deno/Bun)
|
|
8
|
+
* - socket.io (real-time framework)
|
|
9
|
+
*
|
|
10
|
+
* Written to .trickle/websocket.jsonl as:
|
|
11
|
+
* { "kind": "ws", "event": "message", "direction": "in",
|
|
12
|
+
* "data": "...", "timestamp": 1710516000, "url": "ws://..." }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
interface WsEvent {
|
|
19
|
+
kind: 'ws';
|
|
20
|
+
event: 'connect' | 'message' | 'close' | 'error' | 'emit';
|
|
21
|
+
direction?: 'in' | 'out';
|
|
22
|
+
url?: string;
|
|
23
|
+
data?: unknown;
|
|
24
|
+
channel?: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let wsFile: string | null = null;
|
|
29
|
+
const MAX_EVENTS = 200;
|
|
30
|
+
let eventCount = 0;
|
|
31
|
+
const MAX_DATA_LENGTH = 500;
|
|
32
|
+
|
|
33
|
+
function getWsFile(): string {
|
|
34
|
+
if (wsFile) return wsFile;
|
|
35
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
36
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
37
|
+
wsFile = path.join(dir, 'websocket.jsonl');
|
|
38
|
+
try { fs.writeFileSync(wsFile, ''); } catch {};
|
|
39
|
+
return wsFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeWsEvent(event: WsEvent): void {
|
|
43
|
+
if (eventCount >= MAX_EVENTS) return;
|
|
44
|
+
eventCount++;
|
|
45
|
+
try {
|
|
46
|
+
fs.appendFileSync(getWsFile(), JSON.stringify(event) + '\n');
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function truncateData(data: unknown): unknown {
|
|
51
|
+
if (data === undefined || data === null) return data;
|
|
52
|
+
if (typeof data === 'string') {
|
|
53
|
+
return data.length > MAX_DATA_LENGTH ? data.substring(0, MAX_DATA_LENGTH) + '...' : data;
|
|
54
|
+
}
|
|
55
|
+
if (Buffer.isBuffer(data)) {
|
|
56
|
+
return `<Buffer(${data.length} bytes)>`;
|
|
57
|
+
}
|
|
58
|
+
if (typeof data === 'object') {
|
|
59
|
+
try {
|
|
60
|
+
const s = JSON.stringify(data);
|
|
61
|
+
return s.length > MAX_DATA_LENGTH ? JSON.parse(s.substring(0, MAX_DATA_LENGTH) + '..."}}') : data;
|
|
62
|
+
} catch {
|
|
63
|
+
return String(data).substring(0, MAX_DATA_LENGTH);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Patch ws (node WebSocket library) to capture messages.
|
|
71
|
+
*/
|
|
72
|
+
export function patchWs(wsModule: any, debug: boolean): void {
|
|
73
|
+
const WsClass = wsModule.WebSocket || wsModule;
|
|
74
|
+
if (!WsClass?.prototype || (WsClass.prototype as any).__trickle_ws_patched) return;
|
|
75
|
+
|
|
76
|
+
const origSend = WsClass.prototype.send;
|
|
77
|
+
if (origSend) {
|
|
78
|
+
WsClass.prototype.send = function patchedSend(data: any, ...args: any[]) {
|
|
79
|
+
writeWsEvent({
|
|
80
|
+
kind: 'ws',
|
|
81
|
+
event: 'message',
|
|
82
|
+
direction: 'out',
|
|
83
|
+
url: this.url || this._url,
|
|
84
|
+
data: truncateData(data),
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
return origSend.call(this, data, ...args);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Patch 'on' to capture incoming messages
|
|
92
|
+
const origOn = WsClass.prototype.on;
|
|
93
|
+
if (origOn) {
|
|
94
|
+
WsClass.prototype.on = function patchedOn(event: string, listener: any, ...args: any[]) {
|
|
95
|
+
if (event === 'message') {
|
|
96
|
+
const wrappedListener = (data: any, ...rest: any[]) => {
|
|
97
|
+
writeWsEvent({
|
|
98
|
+
kind: 'ws',
|
|
99
|
+
event: 'message',
|
|
100
|
+
direction: 'in',
|
|
101
|
+
url: this.url || this._url,
|
|
102
|
+
data: truncateData(data),
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
return listener(data, ...rest);
|
|
106
|
+
};
|
|
107
|
+
return origOn.call(this, event, wrappedListener, ...args);
|
|
108
|
+
}
|
|
109
|
+
if (event === 'open') {
|
|
110
|
+
const wrappedListener = (...rest: any[]) => {
|
|
111
|
+
writeWsEvent({
|
|
112
|
+
kind: 'ws',
|
|
113
|
+
event: 'connect',
|
|
114
|
+
url: this.url || this._url,
|
|
115
|
+
timestamp: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
return listener(...rest);
|
|
118
|
+
};
|
|
119
|
+
return origOn.call(this, event, wrappedListener, ...args);
|
|
120
|
+
}
|
|
121
|
+
if (event === 'close') {
|
|
122
|
+
const wrappedListener = (code: number, reason: string, ...rest: any[]) => {
|
|
123
|
+
writeWsEvent({
|
|
124
|
+
kind: 'ws',
|
|
125
|
+
event: 'close',
|
|
126
|
+
url: this.url || this._url,
|
|
127
|
+
data: { code, reason: String(reason || '').substring(0, 100) },
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
return listener(code, reason, ...rest);
|
|
131
|
+
};
|
|
132
|
+
return origOn.call(this, event, wrappedListener, ...args);
|
|
133
|
+
}
|
|
134
|
+
return origOn.call(this, event, listener, ...args);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
(WsClass.prototype as any).__trickle_ws_patched = true;
|
|
139
|
+
if (debug) console.log('[trickle/ws] WebSocket tracing enabled');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Patch socket.io client to capture emit/on events.
|
|
144
|
+
*/
|
|
145
|
+
export function patchSocketIo(ioModule: any, debug: boolean): void {
|
|
146
|
+
// socket.io-client: the default export is a function that returns a Socket
|
|
147
|
+
const Socket = ioModule.Socket || (ioModule.io && ioModule.io.Socket);
|
|
148
|
+
if (!Socket?.prototype || (Socket.prototype as any).__trickle_sio_patched) return;
|
|
149
|
+
|
|
150
|
+
const origEmit = Socket.prototype.emit;
|
|
151
|
+
if (origEmit) {
|
|
152
|
+
Socket.prototype.emit = function patchedEmit(event: string, ...args: any[]) {
|
|
153
|
+
if (event !== 'connect' && event !== 'disconnect' && !event.startsWith('__')) {
|
|
154
|
+
writeWsEvent({
|
|
155
|
+
kind: 'ws',
|
|
156
|
+
event: 'emit',
|
|
157
|
+
direction: 'out',
|
|
158
|
+
channel: event,
|
|
159
|
+
data: truncateData(args[0]),
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return origEmit.call(this, event, ...args);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const origOn = Socket.prototype.on;
|
|
168
|
+
if (origOn) {
|
|
169
|
+
Socket.prototype.on = function patchedOn(event: string, listener: any, ...rest: any[]) {
|
|
170
|
+
if (event !== 'connect' && event !== 'disconnect' && !event.startsWith('__')) {
|
|
171
|
+
const wrappedListener = (...args: any[]) => {
|
|
172
|
+
writeWsEvent({
|
|
173
|
+
kind: 'ws',
|
|
174
|
+
event: 'emit',
|
|
175
|
+
direction: 'in',
|
|
176
|
+
channel: event,
|
|
177
|
+
data: truncateData(args[0]),
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
});
|
|
180
|
+
return listener(...args);
|
|
181
|
+
};
|
|
182
|
+
return origOn.call(this, event, wrappedListener, ...rest);
|
|
183
|
+
}
|
|
184
|
+
return origOn.call(this, event, listener, ...rest);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
(Socket.prototype as any).__trickle_sio_patched = true;
|
|
189
|
+
if (debug) console.log('[trickle/ws] socket.io tracing enabled');
|
|
190
|
+
}
|