vedatrace 0.1.8 → 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/README.md CHANGED
@@ -72,6 +72,10 @@ const logger = vedatrace({
72
72
  mask: '[REDACTED]'
73
73
  },
74
74
 
75
+ // Advanced
76
+ immediateFlush: false, // Flush on each log (dev mode, edge runtimes)
77
+ runtime: 'auto', // Force runtime: 'node' | 'browser' | 'cloudflare' | 'deno' | 'bun'
78
+
75
79
  // Callbacks
76
80
  onError: (err) => console.error('VedaTrace error:', err),
77
81
  onSuccess: () => console.log('Logs sent')
@@ -174,13 +178,15 @@ function MyComponent() {
174
178
  ```typescript
175
179
  import { vedatrace } from 'vedatrace'
176
180
 
181
+ // Works seamlessly - no special configuration needed
182
+ // Timer starts automatically on first log (in handler context)
183
+ const logger = vedatrace({
184
+ apiKey: env.VEDATRACE_API_KEY,
185
+ service: 'worker'
186
+ })
187
+
177
188
  export default {
178
189
  async fetch(req, env, ctx) {
179
- const logger = vedatrace({
180
- apiKey: env.VEDATRACE_API_KEY,
181
- service: 'worker'
182
- })
183
-
184
190
  logger.info('Request received', {
185
191
  method: req.method,
186
192
  url: req.url
@@ -194,6 +200,8 @@ export default {
194
200
  }
195
201
  ```
196
202
 
203
+ The SDK automatically detects edge runtimes (Cloudflare Workers, Deno, Bun) and uses lazy timer initialization - the flush timer starts on the first log call, which happens inside your handler where async I/O is allowed.
204
+
197
205
  ## Advanced Usage
198
206
 
199
207
  ### Custom Transports
@@ -308,6 +316,18 @@ Manually flush pending logs. Returns a Promise.
308
316
 
309
317
  Stop the background flush timer. Call this for explicit cleanup in long-running processes or before shutdown.
310
318
 
319
+ ### `logger.start()`
320
+
321
+ Manually start the flush timer. Usually not needed - the timer starts automatically on first log. Useful if you want to ensure the timer is running before any logs are sent.
322
+
323
+ ### `logger.runtime`
324
+
325
+ Get the detected runtime environment. Returns: `'node' | 'browser' | 'cloudflare' | 'deno' | 'bun' | 'edge'`
326
+
327
+ ```typescript
328
+ console.log(logger.runtime) // 'cloudflare' when running in Workers
329
+ ```
330
+
311
331
  ## License
312
332
 
313
333
  MIT
package/dist/index.cjs CHANGED
@@ -5,26 +5,60 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var transports_index = require('./transports/index.cjs');
6
6
 
7
7
  class VedaTraceBatcher {
8
- constructor(transports, config, onError, onSuccess, immediateFlush = false) {
8
+ constructor(transports, config, immediateFlush = false) {
9
9
  this.transports = transports;
10
10
  this.config = config;
11
- this.onError = onError;
12
- this.onSuccess = onSuccess;
13
11
  this.immediateFlush = immediateFlush;
14
- this.startFlushTimer();
12
+ this.context = config.executionContext;
15
13
  }
16
14
  queue = [];
17
15
  flushTimer = null;
16
+ flushDebounceTimer = null;
18
17
  isFlushing = false;
19
18
  pendingFlush = null;
20
- /** Add log to queue */
19
+ context;
20
+ /** Attach execution context after initialization */
21
+ setContext(ctx) {
22
+ this.context = ctx;
23
+ }
24
+ /** Get current context */
25
+ getContext() {
26
+ return this.context;
27
+ }
28
+ /** Add log to queue with context-aware flush */
21
29
  add(log) {
22
30
  this.queue.push(log);
23
- if (this.immediateFlush || this.queue.length >= this.config.batchSize) {
31
+ if (!this.flushTimer && !this.immediateFlush) {
32
+ this.startFlushTimer();
33
+ }
34
+ if (this.immediateFlush || this.context) {
35
+ this.debouncedFlush();
36
+ } else if (this.queue.length >= this.config.batchSize) {
24
37
  this.flush();
25
38
  }
26
39
  }
27
- /** Flush logs to all transports */
40
+ /** Debounced flush - prevents rapid-fire flushes */
41
+ debouncedFlush() {
42
+ if (this.flushDebounceTimer) {
43
+ clearTimeout(this.flushDebounceTimer);
44
+ }
45
+ this.flushDebounceTimer = setTimeout(() => {
46
+ this.flushDebounceTimer = null;
47
+ this.flush().catch((error) => {
48
+ if (this.config.onError) {
49
+ this.config.onError(
50
+ error instanceof Error ? error : new Error(String(error))
51
+ );
52
+ } else {
53
+ console.error(
54
+ "[VedaTrace] Debounced flush error:",
55
+ error instanceof Error ? error.message : String(error)
56
+ );
57
+ }
58
+ });
59
+ }, 100);
60
+ }
61
+ /** Flush logs to all transports with waitUntil protection */
28
62
  async flush() {
29
63
  if (this.isFlushing) {
30
64
  return this.pendingFlush ?? Promise.resolve();
@@ -35,13 +69,16 @@ class VedaTraceBatcher {
35
69
  this.isFlushing = true;
36
70
  const logsToSend = [...this.queue];
37
71
  this.queue = [];
38
- this.pendingFlush = this.sendWithRetry(logsToSend).finally(() => {
72
+ const flushPromise = this.sendWithRetry(logsToSend).finally(() => {
39
73
  this.isFlushing = false;
40
74
  this.pendingFlush = null;
41
75
  });
42
- return this.pendingFlush;
76
+ this.pendingFlush = flushPromise;
77
+ if (this.context) {
78
+ this.context.waitUntil(flushPromise);
79
+ }
80
+ return flushPromise;
43
81
  }
44
- /** Send logs with retry logic */
45
82
  async sendWithRetry(logs, attempt = 0) {
46
83
  const errors = [];
47
84
  for (const transport of this.transports) {
@@ -59,16 +96,17 @@ class VedaTraceBatcher {
59
96
  const combinedError = new Error(
60
97
  `Failed to send logs after ${this.config.maxRetries} retries: ${errors.map((e) => e.message).join(", ")}`
61
98
  );
62
- if (this.onError) {
63
- this.onError(combinedError);
99
+ if (this.config.onError) {
100
+ this.config.onError(combinedError);
64
101
  } else {
65
102
  console.error("[VedaTrace]", combinedError.message);
66
103
  }
67
104
  return;
68
105
  }
69
- this.onSuccess?.();
106
+ if (this.config.onSuccess) {
107
+ this.config.onSuccess();
108
+ }
70
109
  }
71
- /** Start the flush interval timer */
72
110
  startFlushTimer() {
73
111
  if (this.flushTimer) {
74
112
  clearInterval(this.flushTimer);
@@ -76,8 +114,8 @@ class VedaTraceBatcher {
76
114
  this.flushTimer = setInterval(() => {
77
115
  if (this.queue.length > 0) {
78
116
  this.flush().catch((error) => {
79
- if (this.onError) {
80
- this.onError(
117
+ if (this.config.onError) {
118
+ this.config.onError(
81
119
  error instanceof Error ? error : new Error(String(error))
82
120
  );
83
121
  } else {
@@ -93,30 +131,84 @@ class VedaTraceBatcher {
93
131
  this.flushTimer.unref();
94
132
  }
95
133
  }
96
- /** Stop the flush timer */
97
134
  stop() {
98
135
  if (this.flushTimer) {
99
136
  clearInterval(this.flushTimer);
100
137
  this.flushTimer = null;
101
138
  }
139
+ if (this.flushDebounceTimer) {
140
+ clearTimeout(this.flushDebounceTimer);
141
+ this.flushDebounceTimer = null;
142
+ }
143
+ }
144
+ start() {
145
+ if (!this.flushTimer && !this.immediateFlush) {
146
+ this.startFlushTimer();
147
+ }
102
148
  }
103
- /** Delay helper */
104
149
  delay(ms) {
105
150
  return new Promise((resolve) => setTimeout(resolve, ms));
106
151
  }
107
- /** Get current queue size */
108
152
  getQueueSize() {
109
153
  return this.queue.length;
110
154
  }
155
+ setExecutionContext(ctx) {
156
+ this.context = ctx;
157
+ }
158
+ }
159
+
160
+ function detectRuntime() {
161
+ if (typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers") {
162
+ return "cloudflare";
163
+ }
164
+ const g = globalThis;
165
+ if (g?.Deno && g.Deno.version?.deno) {
166
+ return "deno";
167
+ }
168
+ if (g?.Bun) {
169
+ return "bun";
170
+ }
171
+ if (typeof g?.WebSocketPair !== "undefined") {
172
+ return "cloudflare";
173
+ }
174
+ if (typeof process !== "undefined" && process.versions?.node) {
175
+ return "node";
176
+ }
177
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
178
+ return "browser";
179
+ }
180
+ if (typeof fetch !== "undefined" && typeof window === "undefined" && typeof process === "undefined") {
181
+ return "edge";
182
+ }
183
+ return "edge";
184
+ }
185
+ function isEdgeRuntime() {
186
+ const runtime = detectRuntime();
187
+ return runtime === "cloudflare" || runtime === "deno" || runtime === "bun" || runtime === "edge";
188
+ }
189
+ function isServerless() {
190
+ const runtime = detectRuntime();
191
+ return runtime === "cloudflare" || runtime === "edge";
192
+ }
193
+ function isLongRunning() {
194
+ const runtime = detectRuntime();
195
+ return runtime === "node" || runtime === "bun" || runtime === "deno";
196
+ }
197
+ function isBrowser() {
198
+ return detectRuntime() === "browser";
111
199
  }
112
200
 
113
- const SDK_VERSION = "1.0.0";
201
+ const SDK_VERSION = process.env.npm_package_version ?? "0.0.0";
114
202
  class VedaTraceLogger {
115
203
  batcher = null;
204
+ runtime;
116
205
  config;
117
206
  childDefaults;
207
+ _context;
118
208
  constructor(config = {}, childDefaults = {}) {
209
+ this.runtime = config.runtime ?? detectRuntime();
119
210
  this.childDefaults = childDefaults;
211
+ this._context = config.executionContext;
120
212
  this.config = {
121
213
  service: config.service,
122
214
  apiKey: config.apiKey,
@@ -126,61 +218,68 @@ class VedaTraceLogger {
126
218
  flushInterval: config.flushInterval ?? 5e3,
127
219
  maxRetries: config.maxRetries ?? 3,
128
220
  retryDelay: config.retryDelay ?? 1e3,
129
- onError: config.onError,
130
- onSuccess: config.onSuccess,
131
221
  debug: config.debug ?? false,
132
222
  immediateFlush: config.immediateFlush ?? false,
133
- unrefTimer: config.unrefTimer
223
+ unrefTimer: config.unrefTimer ?? false
134
224
  };
135
225
  if (!config.disabled) {
136
226
  this.initializeBatcher(config);
137
227
  }
138
228
  }
139
- /** Initialize the batcher with transports */
140
229
  initializeBatcher(config) {
141
230
  const transports = config.transports ?? [];
142
- if (config.apiKey && transports.length === 0) ;
143
231
  if (transports.length > 0) {
232
+ const batcherConfig = {
233
+ batchSize: this.config.batchSize,
234
+ flushInterval: this.config.flushInterval,
235
+ maxRetries: this.config.maxRetries,
236
+ retryDelay: this.config.retryDelay,
237
+ unrefTimer: this.config.unrefTimer,
238
+ executionContext: this._context,
239
+ onError: config.onError,
240
+ onSuccess: config.onSuccess
241
+ };
144
242
  this.batcher = new VedaTraceBatcher(
145
243
  transports,
146
- {
147
- batchSize: this.config.batchSize,
148
- flushInterval: this.config.flushInterval,
149
- maxRetries: this.config.maxRetries,
150
- retryDelay: this.config.retryDelay,
151
- unrefTimer: this.config.unrefTimer
152
- },
153
- this.config.onError,
154
- this.config.onSuccess,
244
+ batcherConfig,
155
245
  this.config.immediateFlush
156
246
  );
157
247
  }
158
248
  }
159
- /** Set batcher (called from factory function) */
160
249
  setBatcher(batcher) {
161
250
  this.batcher = batcher;
162
251
  }
163
- /** Log at debug level */
252
+ /** Attach execution context for waitUntil support (Cloudflare Workers / Pages) */
253
+ withContext(ctx) {
254
+ this._context = ctx;
255
+ if (this.batcher) {
256
+ this.batcher.setContext(ctx);
257
+ }
258
+ return this;
259
+ }
260
+ /** Check if context is attached */
261
+ hasContext() {
262
+ return this._context !== void 0;
263
+ }
264
+ /** Get current execution context */
265
+ getContext() {
266
+ return this._context;
267
+ }
164
268
  debug(message, metadata) {
165
269
  this.log("debug", message, metadata);
166
270
  }
167
- /** Log at info level */
168
271
  info(message, metadata) {
169
272
  this.log("info", message, metadata);
170
273
  }
171
- /** Log at warn level */
172
274
  warn(message, metadata) {
173
275
  this.log("warn", message, metadata);
174
276
  }
175
- /** Log at error level */
176
277
  error(message, metadata) {
177
278
  this.log("error", message, metadata);
178
279
  }
179
- /** Log at fatal level */
180
280
  fatal(message, metadata) {
181
281
  this.log("fatal", message, metadata);
182
282
  }
183
- /** Internal log method */
184
283
  log(level, message, metadata) {
185
284
  if (!this.batcher) {
186
285
  return;
@@ -195,7 +294,7 @@ class VedaTraceLogger {
195
294
  timestamp: Date.now(),
196
295
  metadata: cleanMetadata,
197
296
  _sdk: {
198
- source: this.detectEnvironment(),
297
+ source: detectRuntime(),
199
298
  version: SDK_VERSION
200
299
  }
201
300
  };
@@ -208,7 +307,6 @@ class VedaTraceLogger {
208
307
  }
209
308
  this.batcher.add(logEntry);
210
309
  }
211
- /** Create a child logger with default metadata */
212
310
  child(defaults) {
213
311
  const mergedDefaults = { ...this.childDefaults, ...defaults };
214
312
  const childLogger = new VedaTraceLogger(
@@ -217,7 +315,8 @@ class VedaTraceLogger {
217
315
  apiKey: this.config.apiKey,
218
316
  endpoint: this.config.endpoint,
219
317
  environment: this.config.environment,
220
- disabled: !this.batcher
318
+ disabled: !this.batcher,
319
+ executionContext: this._context
221
320
  },
222
321
  mergedDefaults
223
322
  );
@@ -226,27 +325,132 @@ class VedaTraceLogger {
226
325
  }
227
326
  return childLogger;
228
327
  }
229
- /** Flush pending logs */
230
328
  async flush() {
231
329
  if (this.batcher) {
232
- await this.batcher.flush();
330
+ const flushPromise = this.batcher.flush();
331
+ if (this._context) {
332
+ this._context.waitUntil(flushPromise);
333
+ }
334
+ return flushPromise;
233
335
  }
234
336
  }
235
- /** Stop the batcher and flush timer */
236
337
  stop() {
237
338
  if (this.batcher) {
238
339
  this.batcher.stop();
239
340
  }
240
341
  }
241
- /** Detect runtime environment */
242
- detectEnvironment() {
243
- if (typeof globalThis !== "undefined" && "navigator" in globalThis) {
244
- return "browser";
342
+ start() {
343
+ if (this.batcher) {
344
+ this.batcher.start();
245
345
  }
246
- if (typeof process !== "undefined" && process.versions?.node) {
247
- return "node";
346
+ }
347
+ }
348
+
349
+ class BrowserLifecycle {
350
+ constructor(config) {
351
+ this.config = config;
352
+ this.boundVisibilityHandler = this.handleVisibilityChange.bind(this);
353
+ this.boundPageHideHandler = this.handlePageHide.bind(this);
354
+ this.boundBeforeUnloadHandler = this.handleBeforeUnload.bind(this);
355
+ this.boundUnloadHandler = this.handleUnload.bind(this);
356
+ }
357
+ boundVisibilityHandler;
358
+ boundPageHideHandler;
359
+ boundBeforeUnloadHandler;
360
+ boundUnloadHandler;
361
+ isAttached = false;
362
+ pendingFlush = null;
363
+ /** Start listening for browser lifecycle events */
364
+ attach() {
365
+ if (this.isAttached) return;
366
+ if (typeof document !== "undefined") {
367
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
368
+ window.addEventListener("pagehide", this.boundPageHideHandler);
369
+ window.addEventListener("beforeunload", this.boundBeforeUnloadHandler);
370
+ window.addEventListener("unload", this.boundUnloadHandler);
371
+ }
372
+ this.isAttached = true;
373
+ if (this.config.debug) {
374
+ console.log("[VedaTrace] Browser lifecycle handlers attached");
375
+ }
376
+ }
377
+ /** Stop listening for browser lifecycle events */
378
+ detach() {
379
+ if (!this.isAttached) return;
380
+ if (typeof document !== "undefined") {
381
+ document.removeEventListener(
382
+ "visibilitychange",
383
+ this.boundVisibilityHandler
384
+ );
385
+ window.removeEventListener("pagehide", this.boundPageHideHandler);
386
+ window.removeEventListener("beforeunload", this.boundBeforeUnloadHandler);
387
+ window.removeEventListener("unload", this.boundUnloadHandler);
388
+ }
389
+ this.isAttached = false;
390
+ if (this.config.debug) {
391
+ console.log("[VedaTrace] Browser lifecycle handlers detached");
248
392
  }
249
- return "edge";
393
+ }
394
+ /** Handle visibility change - flush when page becomes hidden */
395
+ handleVisibilityChange() {
396
+ if (document.visibilityState === "hidden") {
397
+ if (this.config.debug) {
398
+ console.log("[VedaTrace] Page became hidden, flushing logs");
399
+ }
400
+ this.scheduleFlush();
401
+ }
402
+ }
403
+ /** Handle pagehide event - primary flush handler for Safari */
404
+ handlePageHide(event) {
405
+ if (this.config.debug) {
406
+ console.log(
407
+ "[VedaTrace] Page hide event",
408
+ event.persisted ? "(cached)" : "(navigation)"
409
+ );
410
+ }
411
+ if (event.persisted) {
412
+ this.scheduleFlush();
413
+ } else {
414
+ this.finalFlush();
415
+ }
416
+ }
417
+ /** Handle beforeunload - backup flush mechanism */
418
+ handleBeforeUnload(event) {
419
+ if (this.config.debug) {
420
+ console.log("[VedaTrace] Before unload event");
421
+ }
422
+ this.finalFlush();
423
+ }
424
+ /** Handle unload - fallback for older browsers */
425
+ handleUnload() {
426
+ if (this.config.debug) {
427
+ console.log("[VedaTrace] Unload event");
428
+ }
429
+ this.finalFlush();
430
+ }
431
+ /** Schedule a debounced flush (for visibility change) */
432
+ scheduleFlush() {
433
+ if (this.pendingFlush) return;
434
+ this.pendingFlush = this.config.flush().finally(() => {
435
+ this.pendingFlush = null;
436
+ });
437
+ }
438
+ /**
439
+ * Final flush using keepalive fetch
440
+ * For sending logs after the page context is destroyed
441
+ */
442
+ finalFlush() {
443
+ for (const transport of this.config.transports) {
444
+ if (transport.name === "http" && "flush" in transport) {
445
+ transport.flush?.();
446
+ }
447
+ }
448
+ this.config.flush().catch(() => {
449
+ });
450
+ }
451
+ /** Check if handlers are attached */
452
+ isActive() {
453
+ return this.isAttached;
250
454
  }
251
455
  }
252
456
 
@@ -324,26 +528,55 @@ function redactPii(value, mask) {
324
528
  return result;
325
529
  }
326
530
 
531
+ const RUNTIME_FLUSH_INTERVALS = {
532
+ node: 3e3,
533
+ bun: 3e3,
534
+ deno: 3e3,
535
+ browser: 3e3,
536
+ cloudflare: 1e3,
537
+ edge: 1e3
538
+ };
327
539
  function vedatrace(config = {}) {
540
+ const runtime = detectRuntime();
328
541
  const logger = new VedaTraceLogger(config);
329
542
  if (config.apiKey && (!config.transports || config.transports.length === 0)) {
330
- const httpConfig = { apiKey: config.apiKey };
543
+ const isBrowserEnv = isBrowser();
544
+ const isServerlessEnv = isServerless();
545
+ const isLongRunningEnv = isLongRunning();
546
+ const HttpTransport = isBrowserEnv ? transports_index.VedaTraceHttpTransportBrowser : transports_index.VedaTraceHttpTransport;
547
+ const httpConfig = {
548
+ apiKey: config.apiKey,
549
+ keepalive: isBrowserEnv
550
+ };
331
551
  if (config.endpoint) httpConfig.endpoint = config.endpoint;
332
- const httpTransport = new transports_index.VedaTraceHttpTransport(httpConfig);
552
+ const httpTransport = new HttpTransport(httpConfig);
553
+ let immediateFlush = config.immediateFlush ?? false;
554
+ let shouldUnrefTimer = false;
555
+ if (isServerlessEnv) {
556
+ immediateFlush = config.immediateFlush ?? !config.executionContext;
557
+ } else if (isLongRunningEnv) {
558
+ immediateFlush = config.immediateFlush ?? false;
559
+ shouldUnrefTimer = true;
560
+ } else if (isBrowserEnv) {
561
+ immediateFlush = config.immediateFlush ?? false;
562
+ }
563
+ const flushInterval = config.flushInterval ?? RUNTIME_FLUSH_INTERVALS[runtime] ?? 3e3;
333
564
  const batcher = new VedaTraceBatcher(
334
565
  [httpTransport],
335
566
  {
336
567
  batchSize: config.batchSize ?? 100,
337
- flushInterval: config.flushInterval ?? 1e3,
568
+ flushInterval,
338
569
  maxRetries: config.maxRetries ?? 3,
339
570
  retryDelay: config.retryDelay ?? 1e3,
340
- unrefTimer: config.unrefTimer
571
+ unrefTimer: config.unrefTimer ?? shouldUnrefTimer,
572
+ executionContext: config.executionContext,
573
+ onError: config.onError,
574
+ onSuccess: config.onSuccess
341
575
  },
342
- config.onError,
343
- config.onSuccess
576
+ immediateFlush
344
577
  );
345
578
  logger.setBatcher(batcher);
346
- if (typeof process !== "undefined") {
579
+ if (typeof process !== "undefined" && isLongRunningEnv) {
347
580
  const flushLogs = async () => {
348
581
  await batcher.flush();
349
582
  };
@@ -351,6 +584,15 @@ function vedatrace(config = {}) {
351
584
  process.on("SIGTERM", flushLogs);
352
585
  process.on("SIGINT", flushLogs);
353
586
  }
587
+ if (isBrowserEnv) {
588
+ const lifecycle = new BrowserLifecycle({
589
+ transports: [httpTransport],
590
+ flush: () => batcher.flush(),
591
+ debug: config.debug
592
+ });
593
+ lifecycle.attach();
594
+ logger._lifecycle = lifecycle;
595
+ }
354
596
  }
355
597
  return logger;
356
598
  }
@@ -369,9 +611,16 @@ function devVedatrace(config = {}) {
369
611
 
370
612
  exports.VedaTraceConsoleTransport = transports_index.VedaTraceConsoleTransport;
371
613
  exports.VedaTraceHttpTransport = transports_index.VedaTraceHttpTransport;
614
+ exports.VedaTraceHttpTransportBrowser = transports_index.VedaTraceHttpTransportBrowser;
615
+ exports.BrowserLifecycle = BrowserLifecycle;
372
616
  exports.VedaTraceBatcher = VedaTraceBatcher;
373
617
  exports.VedaTraceLogger = VedaTraceLogger;
374
618
  exports.default = vedatrace;
619
+ exports.detectRuntime = detectRuntime;
375
620
  exports.devVedatrace = devVedatrace;
621
+ exports.isBrowser = isBrowser;
622
+ exports.isEdgeRuntime = isEdgeRuntime;
623
+ exports.isLongRunning = isLongRunning;
624
+ exports.isServerless = isServerless;
376
625
  exports.redact = redact;
377
626
  exports.vedatrace = vedatrace;