taist 1.1.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/.taistrc.json +15 -0
- package/LICENSE +20 -0
- package/README.md +782 -0
- package/index.js +147 -0
- package/lib/ast-transformer.js +282 -0
- package/lib/execution-tracer.js +370 -0
- package/lib/loader-hooks.js +71 -0
- package/lib/loader.js +154 -0
- package/lib/output-formatter.js +151 -0
- package/lib/spawn-traced.js +204 -0
- package/lib/toon-formatter.js +351 -0
- package/lib/traced.js +223 -0
- package/lib/tracer-setup.js +115 -0
- package/lib/vitest-plugin.js +138 -0
- package/lib/vitest-runner.js +326 -0
- package/lib/watch-handler.js +300 -0
- package/package.json +71 -0
- package/taist.js +294 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Tracer - Runtime execution tracing without explicit logging
|
|
3
|
+
* Captures function calls, arguments, return values, and errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ExecutionTracer {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.enabled = options.enabled !== false;
|
|
9
|
+
this.depth = options.depth || 2;
|
|
10
|
+
this.maxEntries = options.maxEntries || 1000;
|
|
11
|
+
|
|
12
|
+
// Trace buffer (circular buffer)
|
|
13
|
+
this.traces = [];
|
|
14
|
+
this.currentIndex = 0;
|
|
15
|
+
|
|
16
|
+
// Call stack tracking
|
|
17
|
+
this.callStack = [];
|
|
18
|
+
this.currentDepth = 0;
|
|
19
|
+
|
|
20
|
+
// Performance tracking
|
|
21
|
+
this.startTimes = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start tracing
|
|
26
|
+
*/
|
|
27
|
+
start() {
|
|
28
|
+
this.enabled = true;
|
|
29
|
+
this.traces = [];
|
|
30
|
+
this.currentIndex = 0;
|
|
31
|
+
this.callStack = [];
|
|
32
|
+
this.currentDepth = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stop tracing
|
|
37
|
+
*/
|
|
38
|
+
stop() {
|
|
39
|
+
this.enabled = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clear all traces
|
|
44
|
+
*/
|
|
45
|
+
clear() {
|
|
46
|
+
this.traces = [];
|
|
47
|
+
this.currentIndex = 0;
|
|
48
|
+
this.callStack = [];
|
|
49
|
+
this.currentDepth = 0;
|
|
50
|
+
this.startTimes.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all traces
|
|
55
|
+
*/
|
|
56
|
+
getTraces() {
|
|
57
|
+
return this.traces.filter(t => t !== undefined);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record function entry
|
|
62
|
+
*/
|
|
63
|
+
enter(name, args = []) {
|
|
64
|
+
if (!this.enabled) return;
|
|
65
|
+
if (this.currentDepth >= this.depth) return;
|
|
66
|
+
|
|
67
|
+
const traceId = this.generateId();
|
|
68
|
+
const entry = {
|
|
69
|
+
id: traceId,
|
|
70
|
+
name,
|
|
71
|
+
type: 'enter',
|
|
72
|
+
args: this.shouldCaptureArgs() ? this.sanitizeArgs(args) : undefined,
|
|
73
|
+
depth: this.currentDepth,
|
|
74
|
+
timestamp: Date.now()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.callStack.push(traceId);
|
|
78
|
+
this.currentDepth++;
|
|
79
|
+
this.startTimes.set(traceId, performance.now());
|
|
80
|
+
|
|
81
|
+
this.addTrace(entry);
|
|
82
|
+
|
|
83
|
+
return traceId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Record function exit
|
|
88
|
+
*/
|
|
89
|
+
exit(name, result) {
|
|
90
|
+
if (!this.enabled) return;
|
|
91
|
+
if (this.callStack.length === 0) return;
|
|
92
|
+
|
|
93
|
+
const traceId = this.callStack.pop();
|
|
94
|
+
this.currentDepth = Math.max(0, this.currentDepth - 1);
|
|
95
|
+
|
|
96
|
+
const startTime = this.startTimes.get(traceId);
|
|
97
|
+
const duration = startTime ? performance.now() - startTime : 0;
|
|
98
|
+
this.startTimes.delete(traceId);
|
|
99
|
+
|
|
100
|
+
const entry = {
|
|
101
|
+
id: traceId,
|
|
102
|
+
name,
|
|
103
|
+
type: 'exit',
|
|
104
|
+
result: this.shouldCaptureResult() ? this.sanitizeValue(result) : undefined,
|
|
105
|
+
duration,
|
|
106
|
+
depth: this.currentDepth,
|
|
107
|
+
timestamp: Date.now()
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.addTrace(entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record an error
|
|
115
|
+
*/
|
|
116
|
+
error(name, error) {
|
|
117
|
+
if (!this.enabled) return;
|
|
118
|
+
|
|
119
|
+
const traceId = this.callStack.length > 0 ? this.callStack[this.callStack.length - 1] : this.generateId();
|
|
120
|
+
|
|
121
|
+
const entry = {
|
|
122
|
+
id: traceId,
|
|
123
|
+
name,
|
|
124
|
+
type: 'error',
|
|
125
|
+
error: this.sanitizeError(error),
|
|
126
|
+
depth: this.currentDepth,
|
|
127
|
+
timestamp: Date.now()
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
this.addTrace(entry);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Record a custom event
|
|
135
|
+
*/
|
|
136
|
+
event(name, data = {}) {
|
|
137
|
+
if (!this.enabled) return;
|
|
138
|
+
|
|
139
|
+
const entry = {
|
|
140
|
+
id: this.generateId(),
|
|
141
|
+
name,
|
|
142
|
+
type: 'event',
|
|
143
|
+
data: this.sanitizeValue(data),
|
|
144
|
+
depth: this.currentDepth,
|
|
145
|
+
timestamp: Date.now()
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.addTrace(entry);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Wrap a function with tracing
|
|
153
|
+
*/
|
|
154
|
+
wrap(fn, name) {
|
|
155
|
+
if (!this.enabled) return fn;
|
|
156
|
+
|
|
157
|
+
const tracer = this;
|
|
158
|
+
return new Proxy(fn, {
|
|
159
|
+
apply(target, thisArg, args) {
|
|
160
|
+
tracer.enter(name || fn.name || 'anonymous', args);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const result = Reflect.apply(target, thisArg, args);
|
|
164
|
+
|
|
165
|
+
// Handle promises
|
|
166
|
+
if (result && typeof result.then === 'function') {
|
|
167
|
+
return result
|
|
168
|
+
.then(value => {
|
|
169
|
+
tracer.exit(name || fn.name || 'anonymous', value);
|
|
170
|
+
return value;
|
|
171
|
+
})
|
|
172
|
+
.catch(error => {
|
|
173
|
+
tracer.error(name || fn.name || 'anonymous', error);
|
|
174
|
+
throw error;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
tracer.exit(name || fn.name || 'anonymous', result);
|
|
179
|
+
return result;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
tracer.error(name || fn.name || 'anonymous', error);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Add trace to buffer (circular buffer)
|
|
190
|
+
*/
|
|
191
|
+
addTrace(entry) {
|
|
192
|
+
if (this.traces.length < this.maxEntries) {
|
|
193
|
+
this.traces.push(entry);
|
|
194
|
+
} else {
|
|
195
|
+
this.traces[this.currentIndex] = entry;
|
|
196
|
+
this.currentIndex = (this.currentIndex + 1) % this.maxEntries;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate unique ID
|
|
202
|
+
*/
|
|
203
|
+
generateId() {
|
|
204
|
+
return `t${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Determine if should capture args based on depth level
|
|
209
|
+
*/
|
|
210
|
+
shouldCaptureArgs() {
|
|
211
|
+
return this.depth >= 3;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Determine if should capture result based on depth level
|
|
216
|
+
*/
|
|
217
|
+
shouldCaptureResult() {
|
|
218
|
+
return this.depth >= 2;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Sanitize arguments for storage
|
|
223
|
+
*/
|
|
224
|
+
sanitizeArgs(args) {
|
|
225
|
+
if (!Array.isArray(args)) return [];
|
|
226
|
+
return args.map(arg => this.sanitizeValue(arg));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sanitize a value for storage
|
|
231
|
+
*/
|
|
232
|
+
sanitizeValue(value, maxDepth = 2, currentDepth = 0) {
|
|
233
|
+
if (currentDepth >= maxDepth) {
|
|
234
|
+
return '[deep]';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (value === null) return null;
|
|
238
|
+
if (value === undefined) return undefined;
|
|
239
|
+
|
|
240
|
+
const type = typeof value;
|
|
241
|
+
|
|
242
|
+
if (type === 'string') {
|
|
243
|
+
return value.length > 100 ? value.slice(0, 100) + '...' : value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (type === 'number' || type === 'boolean') {
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (type === 'function') {
|
|
251
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(value)) {
|
|
255
|
+
if (value.length === 0) return [];
|
|
256
|
+
if (value.length > 5) {
|
|
257
|
+
return [
|
|
258
|
+
...value.slice(0, 5).map(v => this.sanitizeValue(v, maxDepth, currentDepth + 1)),
|
|
259
|
+
`...(${value.length - 5} more)`
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
return value.map(v => this.sanitizeValue(v, maxDepth, currentDepth + 1));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (type === 'object') {
|
|
266
|
+
if (value instanceof Error) {
|
|
267
|
+
return this.sanitizeError(value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (value instanceof Date) {
|
|
271
|
+
return value.toISOString();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Regular object
|
|
275
|
+
const keys = Object.keys(value);
|
|
276
|
+
if (keys.length === 0) return {};
|
|
277
|
+
if (keys.length > 5) {
|
|
278
|
+
const result = {};
|
|
279
|
+
keys.slice(0, 5).forEach(key => {
|
|
280
|
+
result[key] = this.sanitizeValue(value[key], maxDepth, currentDepth + 1);
|
|
281
|
+
});
|
|
282
|
+
result['...'] = `(${keys.length - 5} more)`;
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const result = {};
|
|
287
|
+
keys.forEach(key => {
|
|
288
|
+
result[key] = this.sanitizeValue(value[key], maxDepth, currentDepth + 1);
|
|
289
|
+
});
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return String(value).slice(0, 50);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Sanitize error object
|
|
298
|
+
*/
|
|
299
|
+
sanitizeError(error) {
|
|
300
|
+
if (typeof error === 'string') {
|
|
301
|
+
return { message: error };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
name: error.name || 'Error',
|
|
306
|
+
message: error.message || String(error),
|
|
307
|
+
stack: error.stack ? error.stack.split('\n').slice(0, 3) : undefined
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get summary of traces
|
|
313
|
+
*/
|
|
314
|
+
getSummary() {
|
|
315
|
+
const traces = this.getTraces();
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
total: traces.length,
|
|
319
|
+
byType: {
|
|
320
|
+
enter: traces.filter(t => t.type === 'enter').length,
|
|
321
|
+
exit: traces.filter(t => t.type === 'exit').length,
|
|
322
|
+
error: traces.filter(t => t.type === 'error').length,
|
|
323
|
+
event: traces.filter(t => t.type === 'event').length
|
|
324
|
+
},
|
|
325
|
+
errors: traces.filter(t => t.type === 'error'),
|
|
326
|
+
duration: traces.length > 0
|
|
327
|
+
? traces[traces.length - 1].timestamp - traces[0].timestamp
|
|
328
|
+
: 0
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Export traces in TOON-friendly format
|
|
334
|
+
*/
|
|
335
|
+
exportForToon() {
|
|
336
|
+
const traces = this.getTraces();
|
|
337
|
+
|
|
338
|
+
return traces
|
|
339
|
+
.filter(t => t.type === 'exit' || t.type === 'error')
|
|
340
|
+
.map(t => ({
|
|
341
|
+
name: t.name,
|
|
342
|
+
duration: t.duration,
|
|
343
|
+
args: t.args,
|
|
344
|
+
result: t.result,
|
|
345
|
+
error: t.error
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create and register a global tracer instance
|
|
352
|
+
* This allows instrumented code to access the tracer via globalThis.__taistTracer
|
|
353
|
+
* @param {Object} options - Tracer options
|
|
354
|
+
* @returns {ExecutionTracer}
|
|
355
|
+
*/
|
|
356
|
+
export function createGlobalTracer(options = {}) {
|
|
357
|
+
const tracer = new ExecutionTracer(options);
|
|
358
|
+
globalThis.__taistTracer = tracer;
|
|
359
|
+
return tracer;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get the global tracer instance
|
|
364
|
+
* @returns {ExecutionTracer|undefined}
|
|
365
|
+
*/
|
|
366
|
+
export function getGlobalTracer() {
|
|
367
|
+
return globalThis.__taistTracer;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export default ExecutionTracer;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taist Node.js Module Hooks - ESM Loader for code instrumentation
|
|
3
|
+
*
|
|
4
|
+
* This file implements Node.js module hooks to instrument code at load time.
|
|
5
|
+
* Use with: node --import ./lib/loader.js your-script.js
|
|
6
|
+
*
|
|
7
|
+
* The hooks intercept module loading and apply AST transformation to add
|
|
8
|
+
* tracing calls to all functions in the loaded code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { transformCode, defaultShouldInstrument } from './ast-transformer.js';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { readFile } from 'fs/promises';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve hook - determines how to resolve module specifiers
|
|
17
|
+
* We use default resolution, just passing through
|
|
18
|
+
*/
|
|
19
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
20
|
+
return nextResolve(specifier, context);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load hook - transforms source code before it's executed
|
|
25
|
+
* This is where we inject instrumentation into the code
|
|
26
|
+
*/
|
|
27
|
+
export async function load(url, context, nextLoad) {
|
|
28
|
+
// Only process file:// URLs (local files)
|
|
29
|
+
if (!url.startsWith('file://')) {
|
|
30
|
+
return nextLoad(url, context);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const filePath = fileURLToPath(url);
|
|
34
|
+
|
|
35
|
+
// Check if this file should be instrumented
|
|
36
|
+
if (!defaultShouldInstrument(filePath, { enabled: true })) {
|
|
37
|
+
return nextLoad(url, context);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Load the original source
|
|
41
|
+
const result = await nextLoad(url, context);
|
|
42
|
+
|
|
43
|
+
// Only transform JavaScript/TypeScript modules
|
|
44
|
+
if (result.format !== 'module' && result.format !== 'commonjs') {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get the source code
|
|
49
|
+
let source;
|
|
50
|
+
if (result.source) {
|
|
51
|
+
source = typeof result.source === 'string'
|
|
52
|
+
? result.source
|
|
53
|
+
: new TextDecoder().decode(result.source);
|
|
54
|
+
} else {
|
|
55
|
+
// If source isn't provided, read it from the file
|
|
56
|
+
source = await readFile(filePath, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Transform the code
|
|
60
|
+
const transformed = transformCode(source, filePath, { debug: process.env.TAIST_DEBUG });
|
|
61
|
+
|
|
62
|
+
if (transformed) {
|
|
63
|
+
return {
|
|
64
|
+
...result,
|
|
65
|
+
source: transformed.code,
|
|
66
|
+
shortCircuit: true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
package/lib/loader.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taist Loader - Entry point for Node.js module instrumentation
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* node --import ./lib/loader.js your-script.js
|
|
6
|
+
*
|
|
7
|
+
* Or set NODE_OPTIONS:
|
|
8
|
+
* NODE_OPTIONS="--import ./lib/loader.js" node your-script.js
|
|
9
|
+
*
|
|
10
|
+
* Environment variables:
|
|
11
|
+
* TAIST_TRACE_FILE - Path to write traces (required for trace collection)
|
|
12
|
+
* TAIST_DEBUG - Enable debug output
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { register } from 'node:module';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
// Get the directory of this file
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// Register the module hooks
|
|
25
|
+
register('./loader-hooks.js', pathToFileURL(__dirname + '/'));
|
|
26
|
+
|
|
27
|
+
// Set up the global tracer
|
|
28
|
+
const traces = [];
|
|
29
|
+
const callStack = [];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Safely serialize a value for tracing
|
|
33
|
+
* Handles circular references and large objects
|
|
34
|
+
*/
|
|
35
|
+
function safeSerialize(value, depth = 0) {
|
|
36
|
+
if (depth > 2) return '[max depth]';
|
|
37
|
+
if (value === undefined) return undefined;
|
|
38
|
+
if (value === null) return null;
|
|
39
|
+
if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`;
|
|
40
|
+
if (typeof value === 'symbol') return value.toString();
|
|
41
|
+
if (typeof value === 'bigint') return value.toString() + 'n';
|
|
42
|
+
if (typeof value !== 'object') return value;
|
|
43
|
+
|
|
44
|
+
// Handle arrays
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
if (value.length > 5) {
|
|
47
|
+
return [...value.slice(0, 5).map(v => safeSerialize(v, depth + 1)), `...+${value.length - 5}`];
|
|
48
|
+
}
|
|
49
|
+
return value.map(v => safeSerialize(v, depth + 1));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle objects
|
|
53
|
+
try {
|
|
54
|
+
const keys = Object.keys(value);
|
|
55
|
+
if (keys.length > 5) {
|
|
56
|
+
const result = {};
|
|
57
|
+
keys.slice(0, 5).forEach(k => {
|
|
58
|
+
result[k] = safeSerialize(value[k], depth + 1);
|
|
59
|
+
});
|
|
60
|
+
result['...'] = `+${keys.length - 5} more`;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
const result = {};
|
|
64
|
+
keys.forEach(k => {
|
|
65
|
+
result[k] = safeSerialize(value[k], depth + 1);
|
|
66
|
+
});
|
|
67
|
+
return result;
|
|
68
|
+
} catch {
|
|
69
|
+
return '[unserializable]';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Install the global tracer
|
|
74
|
+
globalThis.__taistTracer = {
|
|
75
|
+
enter(name, args) {
|
|
76
|
+
const startTime = performance.now();
|
|
77
|
+
callStack.push({ name, startTime, args: safeSerialize(args) });
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
exit(name, result) {
|
|
81
|
+
const entry = callStack.pop();
|
|
82
|
+
if (!entry) return;
|
|
83
|
+
|
|
84
|
+
const duration = performance.now() - entry.startTime;
|
|
85
|
+
traces.push({
|
|
86
|
+
name: entry.name,
|
|
87
|
+
duration: Math.round(duration * 100) / 100,
|
|
88
|
+
args: entry.args,
|
|
89
|
+
result: safeSerialize(result),
|
|
90
|
+
type: 'exit'
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
error(name, error) {
|
|
95
|
+
const entry = callStack.pop();
|
|
96
|
+
if (!entry) return;
|
|
97
|
+
|
|
98
|
+
const duration = performance.now() - entry.startTime;
|
|
99
|
+
traces.push({
|
|
100
|
+
name: entry.name,
|
|
101
|
+
duration: Math.round(duration * 100) / 100,
|
|
102
|
+
args: entry.args,
|
|
103
|
+
error: {
|
|
104
|
+
name: error?.name || 'Error',
|
|
105
|
+
message: error?.message || String(error)
|
|
106
|
+
},
|
|
107
|
+
type: 'error'
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
getTraces() {
|
|
112
|
+
return traces;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Write traces on process exit
|
|
117
|
+
function writeTraces() {
|
|
118
|
+
const tracePath = process.env.TAIST_TRACE_FILE;
|
|
119
|
+
if (!tracePath || traces.length === 0) return;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Read existing traces and append
|
|
123
|
+
let existing = [];
|
|
124
|
+
if (existsSync(tracePath)) {
|
|
125
|
+
try {
|
|
126
|
+
existing = JSON.parse(readFileSync(tracePath, 'utf-8'));
|
|
127
|
+
} catch {
|
|
128
|
+
existing = [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
writeFileSync(tracePath, JSON.stringify([...existing, ...traces]));
|
|
132
|
+
|
|
133
|
+
if (process.env.TAIST_DEBUG) {
|
|
134
|
+
console.error(`[taist] Wrote ${traces.length} traces to ${tracePath}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error('[taist] Failed to write traces:', e.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Register exit handlers
|
|
142
|
+
process.on('exit', writeTraces);
|
|
143
|
+
process.on('SIGINT', () => {
|
|
144
|
+
writeTraces();
|
|
145
|
+
process.exit(130);
|
|
146
|
+
});
|
|
147
|
+
process.on('SIGTERM', () => {
|
|
148
|
+
writeTraces();
|
|
149
|
+
process.exit(143);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (process.env.TAIST_DEBUG) {
|
|
153
|
+
console.error('[taist] Loader initialized');
|
|
154
|
+
}
|