te.js 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,10 +10,9 @@ import Tejas from 'te.js';
10
10
  const app = new Tejas();
11
11
 
12
12
  app
13
- .withRedis({ url: 'redis://localhost:6379' })
14
13
  .withRateLimit({
15
14
  maxRequests: 100,
16
- timeWindowSeconds: 60
15
+ timeWindowSeconds: 60,
17
16
  })
18
17
  .takeoff();
19
18
  ```
@@ -25,35 +24,35 @@ This limits all endpoints to 100 requests per minute per IP address.
25
24
  ```javascript
26
25
  app.withRateLimit({
27
26
  // Core settings
28
- maxRequests: 100, // Maximum requests in time window
29
- timeWindowSeconds: 60, // Time window in seconds
30
-
27
+ maxRequests: 100, // Maximum requests in time window
28
+ timeWindowSeconds: 60, // Time window in seconds
29
+
31
30
  // Algorithm selection
32
31
  algorithm: 'sliding-window', // 'sliding-window' | 'token-bucket' | 'fixed-window'
33
-
32
+
34
33
  // Storage backend
35
- store: 'redis', // 'redis' | 'memory'
36
-
34
+ store: 'memory', // 'memory' or { type: 'redis', url: '...' }
35
+
37
36
  // Custom key generator (defaults to IP-based)
38
37
  keyGenerator: (ammo) => ammo.ip,
39
-
38
+
40
39
  // Algorithm-specific options
41
40
  algorithmOptions: {},
42
-
41
+
43
42
  // Key prefix for storage keys (useful for namespacing)
44
43
  keyPrefix: 'rl:',
45
-
44
+
46
45
  // Header format
47
46
  headerFormat: {
48
- type: 'standard', // 'standard' | 'legacy' | 'both'
49
- draft7: false, // Include RateLimit-Policy header (e.g. "100;w=60")
50
- draft8: false // Use delta-seconds for RateLimit-Reset instead of Unix timestamp
47
+ type: 'standard', // 'standard' | 'legacy' | 'both'
48
+ draft7: false, // Include RateLimit-Policy header (e.g. "100;w=60")
49
+ draft8: false, // Use delta-seconds for RateLimit-Reset instead of Unix timestamp
51
50
  },
52
-
51
+
53
52
  // Custom handler when rate limited
54
53
  onRateLimited: (ammo) => {
55
54
  ammo.fire(429, { error: 'Slow down!' });
56
- }
55
+ },
57
56
  });
58
57
  ```
59
58
 
@@ -67,7 +66,7 @@ Best for smooth, accurate rate limiting. Prevents the "burst at window boundary"
67
66
  app.withRateLimit({
68
67
  maxRequests: 100,
69
68
  timeWindowSeconds: 60,
70
- algorithm: 'sliding-window'
69
+ algorithm: 'sliding-window',
71
70
  });
72
71
  ```
73
72
 
@@ -83,9 +82,9 @@ app.withRateLimit({
83
82
  timeWindowSeconds: 60,
84
83
  algorithm: 'token-bucket',
85
84
  algorithmOptions: {
86
- refillRate: 1.67, // Tokens per second (100/60)
87
- burstSize: 150 // Maximum tokens (allows 50% burst)
88
- }
85
+ refillRate: 1.67, // Tokens per second (100/60)
86
+ burstSize: 150, // Maximum tokens (allows 50% burst)
87
+ },
89
88
  });
90
89
  ```
91
90
 
@@ -101,8 +100,8 @@ app.withRateLimit({
101
100
  timeWindowSeconds: 60,
102
101
  algorithm: 'fixed-window',
103
102
  algorithmOptions: {
104
- strictWindow: true // Align to clock boundaries
105
- }
103
+ strictWindow: true, // Align to clock boundaries
104
+ },
106
105
  });
107
106
  ```
108
107
 
@@ -118,31 +117,34 @@ Good for single-server deployments:
118
117
  app.withRateLimit({
119
118
  maxRequests: 100,
120
119
  timeWindowSeconds: 60,
121
- store: 'memory'
120
+ store: 'memory',
122
121
  });
123
122
  ```
124
123
 
125
124
  **Pros:** No external dependencies, fast
126
- **Cons:** Not shared between server instances
125
+ **Cons:** Not shared between server instances — may be inaccurate in distributed deployments
126
+
127
+ > **Warning:** If you run multiple server instances (e.g. behind a load balancer), each instance tracks its own counters independently. Use Redis storage below for accurate distributed rate limiting.
127
128
 
128
129
  ### Redis
129
130
 
130
- Required for distributed/multi-server deployments:
131
+ For distributed / multi-instance deployments where counters must be shared:
131
132
 
132
133
  ```javascript
133
- app
134
- .withRedis({ url: 'redis://localhost:6379' })
135
- .withRateLimit({
136
- maxRequests: 100,
137
- timeWindowSeconds: 60,
138
- store: 'redis'
139
- });
134
+ app.withRateLimit({
135
+ maxRequests: 100,
136
+ timeWindowSeconds: 60,
137
+ store: {
138
+ type: 'redis',
139
+ url: 'redis://localhost:6379',
140
+ },
141
+ });
140
142
  ```
141
143
 
142
- **Pros:** Shared across all servers, persistent
143
- **Cons:** Requires Redis server, slightly higher latency
144
+ The `redis` npm package is auto-installed on first use if not already present. Any additional properties in the `store` object are forwarded to the node-redis `createClient` call.
144
145
 
145
- > **Important:** Initialize Redis with `withRedis()` before using `store: 'redis'`
146
+ **Pros:** Shared across all server instances, persistent
147
+ **Cons:** Requires a Redis server, slightly higher latency
146
148
 
147
149
  ## Custom Key Generation
148
150
 
@@ -154,7 +156,7 @@ By default, rate limiting is based on client IP. Customize this:
154
156
  app.withRateLimit({
155
157
  maxRequests: 100,
156
158
  timeWindowSeconds: 60,
157
- keyGenerator: (ammo) => ammo.user?.id || ammo.ip
159
+ keyGenerator: (ammo) => ammo.user?.id || ammo.ip,
158
160
  });
159
161
  ```
160
162
 
@@ -164,7 +166,7 @@ app.withRateLimit({
164
166
  app.withRateLimit({
165
167
  maxRequests: 1000,
166
168
  timeWindowSeconds: 60,
167
- keyGenerator: (ammo) => ammo.headers['x-api-key'] || ammo.ip
169
+ keyGenerator: (ammo) => ammo.headers['x-api-key'] || ammo.ip,
168
170
  });
169
171
  ```
170
172
 
@@ -174,7 +176,7 @@ app.withRateLimit({
174
176
  app.withRateLimit({
175
177
  maxRequests: 100,
176
178
  timeWindowSeconds: 60,
177
- keyGenerator: (ammo) => `${ammo.ip}:${ammo.endpoint}`
179
+ keyGenerator: (ammo) => `${ammo.ip}:${ammo.endpoint}`,
178
180
  });
179
181
  ```
180
182
 
@@ -194,7 +196,7 @@ RateLimit-Reset: 1706540400
194
196
 
195
197
  ```javascript
196
198
  app.withRateLimit({
197
- headerFormat: { type: 'legacy' }
199
+ headerFormat: { type: 'legacy' },
198
200
  });
199
201
  ```
200
202
 
@@ -208,7 +210,7 @@ X-RateLimit-Reset: 1706540400
208
210
 
209
211
  ```javascript
210
212
  app.withRateLimit({
211
- headerFormat: { type: 'both' }
213
+ headerFormat: { type: 'both' },
212
214
  });
213
215
  ```
214
216
 
@@ -216,7 +218,7 @@ app.withRateLimit({
216
218
 
217
219
  ```javascript
218
220
  app.withRateLimit({
219
- headerFormat: { type: 'standard', draft7: true }
221
+ headerFormat: { type: 'standard', draft7: true },
220
222
  });
221
223
  ```
222
224
 
@@ -238,9 +240,9 @@ app.withRateLimit({
238
240
  ammo.fire(429, {
239
241
  error: 'Rate limit exceeded',
240
242
  message: 'Please slow down and try again later',
241
- retryAfter: ammo.res.getHeader('Retry-After')
243
+ retryAfter: ammo.res.getHeader('Retry-After'),
242
244
  });
243
- }
245
+ },
244
246
  });
245
247
  ```
246
248
 
@@ -259,14 +261,14 @@ const api = new Target('/api');
259
261
  const authLimiter = rateLimiter({
260
262
  maxRequests: 5,
261
263
  timeWindowSeconds: 60,
262
- algorithm: 'fixed-window'
264
+ algorithm: 'fixed-window',
263
265
  });
264
266
 
265
267
  // Relaxed limit for read operations
266
268
  const readLimiter = rateLimiter({
267
269
  maxRequests: 1000,
268
270
  timeWindowSeconds: 60,
269
- algorithm: 'sliding-window'
271
+ algorithm: 'sliding-window',
270
272
  });
271
273
 
272
274
  // Apply to specific routes
@@ -281,11 +283,11 @@ api.register('/data', readLimiter, (ammo) => {
281
283
 
282
284
  ## Algorithm Comparison
283
285
 
284
- | Algorithm | Best For | Burst Handling | Accuracy | Memory |
285
- |-----------|----------|----------------|----------|--------|
286
- | **Sliding Window** | Most APIs | Smooth | High | Medium |
287
- | **Token Bucket** | Burst-tolerant APIs | Allows bursts | Medium | Low |
288
- | **Fixed Window** | Simple cases | Poor at boundaries | Low | Low |
286
+ | Algorithm | Best For | Burst Handling | Accuracy | Memory |
287
+ | ------------------ | ------------------- | ------------------ | -------- | ------ |
288
+ | **Sliding Window** | Most APIs | Smooth | High | Medium |
289
+ | **Token Bucket** | Burst-tolerant APIs | Allows bursts | Medium | Low |
290
+ | **Fixed Window** | Simple cases | Poor at boundaries | Low | Low |
289
291
 
290
292
  ## Examples
291
293
 
@@ -295,7 +297,7 @@ api.register('/data', readLimiter, (ammo) => {
295
297
  const tierLimits = {
296
298
  free: { maxRequests: 100, timeWindowSeconds: 3600 },
297
299
  pro: { maxRequests: 1000, timeWindowSeconds: 3600 },
298
- enterprise: { maxRequests: 10000, timeWindowSeconds: 3600 }
300
+ enterprise: { maxRequests: 10000, timeWindowSeconds: 3600 },
299
301
  };
300
302
 
301
303
  app.withRateLimit({
@@ -309,8 +311,8 @@ app.withRateLimit({
309
311
  getLimits: (key) => {
310
312
  const tier = key.split(':')[0];
311
313
  return tierLimits[tier] || tierLimits.free;
312
- }
313
- }
314
+ },
315
+ },
314
316
  });
315
317
  ```
316
318
 
@@ -320,14 +322,13 @@ app.withRateLimit({
320
322
  // Global rate limit
321
323
  app.withRateLimit({
322
324
  maxRequests: 1000,
323
- timeWindowSeconds: 60
325
+ timeWindowSeconds: 60,
324
326
  });
325
327
 
326
328
  // Stricter limit for expensive endpoints
327
329
  const expensiveLimiter = rateLimiter({
328
330
  maxRequests: 10,
329
331
  timeWindowSeconds: 60,
330
- store: 'redis'
331
332
  });
332
333
 
333
334
  api.register('/search', expensiveLimiter, (ammo) => {
@@ -348,7 +349,7 @@ target.register('/status', (ammo) => {
348
349
  ammo.fire({
349
350
  limit: ammo.res.getHeader('RateLimit-Limit'),
350
351
  remaining: ammo.res.getHeader('RateLimit-Remaining'),
351
- reset: ammo.res.getHeader('RateLimit-Reset')
352
+ reset: ammo.res.getHeader('RateLimit-Reset'),
352
353
  });
353
354
  });
354
355
  ```
@@ -380,11 +381,11 @@ class PostgresStorage extends RateLimitStorage {
380
381
  }
381
382
  ```
382
383
 
383
- The built-in backends (`MemoryStorage` and `RedisStorage`) both extend this base class.
384
+ The built-in `MemoryStorage` and `RedisStorage` backends both extend this base class.
384
385
 
385
386
  ## Best Practices
386
387
 
387
- 1. **Use Redis in production** — Memory store doesn't scale across instances
388
+ 1. **Use Redis in production** — Memory store doesn't share counters across instances; use `store: { type: 'redis', url: '...' }` for distributed deployments
388
389
  2. **Set appropriate limits** — Too strict frustrates users, too lenient invites abuse
389
390
  3. **Different limits for different endpoints** — Auth endpoints need stricter limits
390
391
  4. **Include headers** — Help clients self-regulate
package/lib/llm/client.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
8
8
  const DEFAULT_MODEL = 'gpt-4o-mini';
9
- const DEFAULT_TIMEOUT = 10000;
9
+ const DEFAULT_TIMEOUT = 20000;
10
10
 
11
11
  /**
12
12
  * OpenAI-compatible LLM provider. Exposes only constructor and analyze(prompt).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "AI Native Node.js Framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
package/radar/index.js CHANGED
@@ -1,10 +1,22 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { createHash } from 'node:crypto';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { gzip } from 'node:zlib';
5
+ import { promisify } from 'node:util';
6
+
7
+ const gzipAsync = promisify(gzip);
8
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
9
  import TejLogger from 'tej-logger';
5
10
 
6
11
  const logger = new TejLogger('Tejas.Radar');
7
12
 
13
+ /**
14
+ * AsyncLocalStorage instance for propagating trace context across async
15
+ * boundaries within a single request. Middleware sets `{ traceId }` on
16
+ * entry; downstream code can read it via `traceStore.getStore()?.traceId`.
17
+ */
18
+ export const traceStore = new AsyncLocalStorage();
19
+
8
20
  /**
9
21
  * Attempt to read the `name` field from the nearest package.json at startup.
10
22
  * Returns null if the file cannot be read or parsed.
@@ -92,6 +104,9 @@ function parseJsonSafe(raw) {
92
104
  * @param {string} [config.projectName] Project identifier. Falls back to RADAR_PROJECT_NAME env, then package.json `name`, then "tejas-app".
93
105
  * @param {number} [config.flushInterval] Milliseconds between periodic flushes (default 2000).
94
106
  * @param {number} [config.batchSize] Flush immediately when batch reaches this size (default 100).
107
+ * @param {number} [config.maxQueueSize] Maximum events held in memory before oldest are dropped (default 10000).
108
+ * @param {Function} [config.transport] Custom transport `(events) => Promise<{ok, status}>`.
109
+ * Defaults to gzip-compressed HTTP POST to the collector.
95
110
  * @param {string[]} [config.ignore] Request paths to skip (default ['/health']).
96
111
  * @param {Object} [config.capture] Controls what additional data is captured beyond metrics.
97
112
  * @param {boolean} [config.capture.request] Capture and send request body (default false).
@@ -123,6 +138,7 @@ async function radarMiddleware(config = {}) {
123
138
 
124
139
  const flushInterval = config.flushInterval ?? 2000;
125
140
  const batchSize = config.batchSize ?? 100;
141
+ const maxQueueSize = config.maxQueueSize ?? 10_000;
126
142
  const ignorePaths = new Set(config.ignore ?? ['/health']);
127
143
 
128
144
  const capture = Object.freeze({
@@ -139,10 +155,13 @@ async function radarMiddleware(config = {}) {
139
155
  );
140
156
 
141
157
  if (!apiKey) {
142
- logger.warn(
143
- 'No API key provided (config.apiKey or RADAR_API_KEY). Radar telemetry disabled.',
144
- );
145
- return (_ammo, next) => next();
158
+ const mw = (_ammo, next) => next();
159
+ mw._radarStatus = {
160
+ feature: 'Radar',
161
+ ok: null,
162
+ detail: 'disabled (no API key)',
163
+ };
164
+ return mw;
146
165
  }
147
166
 
148
167
  const ingestUrl = `${collectorUrl}/ingest`;
@@ -152,61 +171,113 @@ async function radarMiddleware(config = {}) {
152
171
  /** @type {Array<Object>} */
153
172
  let batch = [];
154
173
  let connected = false;
174
+ let retryQueue = null;
175
+ let retryCount = 0;
176
+ const MAX_RETRIES = 3;
177
+
178
+ async function defaultHttpTransport(events) {
179
+ const json = JSON.stringify(events);
180
+ const compressed = await gzipAsync(Buffer.from(json));
181
+ return fetch(ingestUrl, {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ 'Content-Encoding': 'gzip',
186
+ Authorization: authHeader,
187
+ },
188
+ body: compressed,
189
+ });
190
+ }
155
191
 
156
- logger.info(`Checking Radar connectivity ${collectorUrl}`);
192
+ const send = config.transport ?? defaultHttpTransport;
157
193
 
194
+ /** @type {{ feature: string, ok: boolean, detail: string }} */
195
+ let radarStatus;
158
196
  try {
159
197
  const healthRes = await fetch(healthUrl);
160
198
  if (healthRes.ok) {
161
- logger.info(`Radar collector reachable at ${collectorUrl}`);
199
+ radarStatus = {
200
+ feature: 'Radar',
201
+ ok: true,
202
+ detail: `connected (${collectorUrl})`,
203
+ };
162
204
  } else {
163
- logger.warn(
164
- `Radar collector responded with ${healthRes.status} on /health — check collector status.`,
165
- );
205
+ radarStatus = {
206
+ feature: 'Radar',
207
+ ok: false,
208
+ detail: `collector returned ${healthRes.status} (${collectorUrl})`,
209
+ };
166
210
  }
167
211
  } catch (err) {
168
- logger.warn(
169
- `Radar collector unreachable at ${collectorUrl}: ${err.message}`,
170
- );
212
+ radarStatus = {
213
+ feature: 'Radar',
214
+ ok: false,
215
+ detail: `unreachable (${collectorUrl})`,
216
+ };
171
217
  }
172
218
 
173
- async function flush() {
174
- if (batch.length === 0) return;
175
- const payload = batch;
176
- batch = [];
177
-
219
+ async function sendPayload(payload) {
178
220
  try {
179
- const res = await fetch(ingestUrl, {
180
- method: 'POST',
181
- headers: {
182
- 'Content-Type': 'application/json',
183
- Authorization: authHeader,
184
- },
185
- body: JSON.stringify(payload),
186
- });
187
- if (res.ok && !connected) {
188
- connected = true;
189
- logger.info(
190
- `Connected project: "${projectName}", collector: ${collectorUrl}`,
191
- );
192
- } else if (res.status === 401 && connected) {
193
- connected = false;
194
- logger.warn('Radar API key rejected by collector.');
195
- } else if (res.status === 401) {
221
+ const res = await send(payload);
222
+ if (res.ok) {
223
+ if (!connected) {
224
+ connected = true;
225
+ logger.info(
226
+ `Connected — project: "${projectName}", collector: ${collectorUrl}`,
227
+ );
228
+ }
229
+ return true;
230
+ }
231
+ if (res.status === 401) {
232
+ if (connected) connected = false;
196
233
  logger.warn(
197
234
  'Radar API key rejected by collector. Telemetry will not be recorded.',
198
235
  );
236
+ return true;
199
237
  }
238
+ return false;
200
239
  } catch (err) {
201
240
  logger.warn(`Radar flush failed: ${err.message}`);
241
+ return false;
242
+ }
243
+ }
244
+
245
+ async function flush() {
246
+ if (retryQueue) {
247
+ const ok = await sendPayload(retryQueue);
248
+ if (ok) {
249
+ retryQueue = null;
250
+ retryCount = 0;
251
+ } else {
252
+ retryCount++;
253
+ if (retryCount >= MAX_RETRIES) {
254
+ logger.warn(
255
+ `Radar dropping ${retryQueue.length} events after ${MAX_RETRIES} failed retries`,
256
+ );
257
+ retryQueue = null;
258
+ retryCount = 0;
259
+ }
260
+ return;
261
+ }
262
+ }
263
+
264
+ if (batch.length === 0) return;
265
+ const payload = batch;
266
+ batch = [];
267
+
268
+ const ok = await sendPayload(payload);
269
+ if (!ok) {
270
+ retryQueue = payload;
271
+ retryCount = 1;
202
272
  }
203
273
  }
204
274
 
205
275
  const timer = setInterval(flush, flushInterval);
206
276
  if (timer.unref) timer.unref();
207
277
 
208
- return function radarCapture(ammo, next) {
278
+ function radarCapture(ammo, next) {
209
279
  const startTime = Date.now();
280
+ const traceId = randomUUID().replace(/-/g, '');
210
281
 
211
282
  ammo.res.on('finish', () => {
212
283
  const path = ammo.endpoint ?? ammo.path ?? '/';
@@ -214,68 +285,98 @@ async function radarMiddleware(config = {}) {
214
285
  if (ammo.method === 'OPTIONS' || ignorePaths.has(path)) return;
215
286
 
216
287
  const status = ammo.res.statusCode;
217
- const errorInfo = ammo._errorInfo ?? null;
218
-
219
- // Build structured error JSON for the logs table when an error occurred.
220
- let errorField = null;
221
- if (status >= 400 && errorInfo) {
222
- errorField = JSON.stringify({
223
- message: errorInfo.message ?? null,
224
- type: errorInfo.type ?? null,
225
- devInsight: errorInfo.devInsight ?? null,
226
- codeContext: errorInfo.codeContext ?? null,
227
- });
228
- }
288
+ const endTimestamp = Date.now();
289
+ const duration = endTimestamp - startTime;
290
+ const payloadSize = Buffer.byteLength(
291
+ JSON.stringify(ammo.payload ?? {}),
292
+ 'utf8',
293
+ );
294
+ const responseSize = Buffer.byteLength(ammo.dispatchedData ?? '', 'utf8');
295
+ const ip = ammo.ip ?? null;
296
+ const userAgent = ammo.headers?.['user-agent'] ?? null;
297
+ const headers = buildHeaders(ammo.headers, capture.headers);
298
+ const requestBody = capture.request
299
+ ? deepMask(ammo.payload ?? null, clientMaskBlocklist)
300
+ : null;
301
+ const responseBody = capture.response
302
+ ? deepMask(parseJsonSafe(ammo.dispatchedData), clientMaskBlocklist)
303
+ : null;
304
+
305
+ function pushEvents() {
306
+ const errorInfo = ammo._errorInfo ?? null;
307
+
308
+ let errorField = null;
309
+ if (status >= 400 && errorInfo) {
310
+ errorField = JSON.stringify({
311
+ message: errorInfo.message ?? null,
312
+ type: errorInfo.type ?? null,
313
+ devInsight: errorInfo.devInsight ?? null,
314
+ });
315
+ }
316
+
317
+ const incoming = status >= 400 ? 2 : 1;
318
+ if (batch.length + incoming > maxQueueSize) {
319
+ const overflow = batch.length + incoming - maxQueueSize;
320
+ batch.splice(0, overflow);
321
+ }
229
322
 
230
- batch.push({
231
- type: 'log',
232
- projectName,
233
- method: ammo.method,
234
- path,
235
- status,
236
- duration_ms: Date.now() - startTime,
237
- payload_size: Buffer.byteLength(
238
- JSON.stringify(ammo.payload ?? {}),
239
- 'utf8',
240
- ),
241
- response_size: Buffer.byteLength(ammo.dispatchedData ?? '', 'utf8'),
242
- timestamp: Date.now(),
243
- ip: ammo.ip ?? null,
244
- traceId: null,
245
- user_agent: ammo.headers?.['user-agent'] ?? null,
246
- headers: buildHeaders(ammo.headers, capture.headers),
247
- request_body: capture.request
248
- ? deepMask(ammo.payload ?? null, clientMaskBlocklist)
249
- : null,
250
- response_body: capture.response
251
- ? deepMask(parseJsonSafe(ammo.dispatchedData), clientMaskBlocklist)
252
- : null,
253
- error: errorField,
254
- });
255
-
256
- // Emit a separate ErrorEvent for error grouping and tracking when status >= 400.
257
- if (status >= 400) {
258
- const message = errorInfo?.message ?? `HTTP ${status}`;
259
- const fingerprint = createHash('sha256')
260
- .update(`${message}:${path}`)
261
- .digest('hex');
262
323
  batch.push({
263
- type: 'error',
324
+ type: 'log',
264
325
  projectName,
265
- fingerprint,
266
- message,
267
- stack: errorInfo?.stack ?? null,
268
- endpoint: `${ammo.method} ${path}`,
269
- traceId: null,
270
- timestamp: Date.now(),
326
+ method: ammo.method,
327
+ path,
328
+ status,
329
+ duration_ms: duration,
330
+ payload_size: payloadSize,
331
+ response_size: responseSize,
332
+ timestamp: endTimestamp,
333
+ ip,
334
+ traceId,
335
+ user_agent: userAgent,
336
+ headers,
337
+ request_body: requestBody,
338
+ response_body: responseBody,
339
+ error: errorField,
271
340
  });
341
+
342
+ if (status >= 400) {
343
+ const message = errorInfo?.message ?? `HTTP ${status}`;
344
+ const fingerprint = createHash('sha256')
345
+ .update(`${message}:${path}`)
346
+ .digest('hex');
347
+ batch.push({
348
+ type: 'error',
349
+ projectName,
350
+ fingerprint,
351
+ message,
352
+ stack: errorInfo?.stack ?? null,
353
+ endpoint: `${ammo.method} ${path}`,
354
+ traceId,
355
+ timestamp: endTimestamp,
356
+ });
357
+ }
358
+
359
+ if (batch.length >= batchSize) flush();
272
360
  }
273
361
 
274
- if (batch.length >= batchSize) flush();
362
+ if (ammo._llmPromise) {
363
+ const timeout = new Promise((resolve) => {
364
+ const t = setTimeout(resolve, 30000);
365
+ if (t.unref) t.unref();
366
+ });
367
+ Promise.race([ammo._llmPromise, timeout])
368
+ .catch(() => {})
369
+ .then(pushEvents);
370
+ } else {
371
+ pushEvents();
372
+ }
275
373
  });
276
374
 
277
- next();
278
- };
375
+ traceStore.run({ traceId }, () => next());
376
+ }
377
+
378
+ radarCapture._radarStatus = radarStatus;
379
+ return radarCapture;
279
380
  }
280
381
 
281
382
  export default radarMiddleware;