vedatrace 0.3.0 → 0.3.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.
package/README.md CHANGED
@@ -32,7 +32,7 @@ bun add vedatrace
32
32
  import { vedatrace } from 'vedatrace'
33
33
 
34
34
  const logger = vedatrace({
35
- apiKey: 'your-api-key',
35
+ apiKey: 'your-api-key', // or set VEDATRACE_API_KEY in the environment
36
36
  service: 'my-service'
37
37
  })
38
38
 
@@ -66,12 +66,6 @@ const logger = vedatrace({
66
66
  maxRetries: 3,
67
67
  retryDelay: 1000,
68
68
 
69
- // Redaction
70
- redaction: {
71
- paths: ['password', 'token'],
72
- mask: '[REDACTED]'
73
- },
74
-
75
69
  // Advanced
76
70
  immediateFlush: false, // Flush on each log (dev mode, edge runtimes)
77
71
  runtime: 'auto', // Force runtime: 'node' | 'browser' | 'cloudflare' | 'deno' | 'bun'
@@ -250,6 +244,8 @@ The SDK timer uses `unref()` so the process will exit automatically when there's
250
244
  ### Error Handling
251
245
 
252
246
  ```typescript
247
+ import { vedatrace, VedaTraceTransportError } from 'vedatrace'
248
+
253
249
  const logger = vedatrace({
254
250
  apiKey: '...',
255
251
  onError: (err) => {
@@ -262,6 +258,50 @@ const logger = vedatrace({
262
258
  logger.error('Critical error', { stack: err.stack })
263
259
  ```
264
260
 
261
+ Transport failures arrive as `VedaTraceTransportError`, carrying `status`,
262
+ `retryable`, `fatal` and `retryAfterMs` so you can tell a bad key from a blip:
263
+
264
+ ```typescript
265
+ onError: (err) => {
266
+ if (err instanceof VedaTraceTransportError && err.fatal) {
267
+ // 401 / 403 - the key or its allowed origins are wrong
268
+ }
269
+ }
270
+ ```
271
+
272
+ ### Retry behaviour
273
+
274
+ Failed batches retry with backoff on `408`, `425`, `429` and `5xx`, and on
275
+ network errors. A `Retry-After` header takes precedence over the local backoff.
276
+ Other 4xx responses are reported once and not retried, since a retry cannot
277
+ change the outcome.
278
+
279
+ A `401` or `403` **halts the logger**: the credentials will fail identically on
280
+ every future batch, so the SDK reports once through `onError` and stops instead
281
+ of spending a doomed request per flush. Fix the key (or the key's allowed
282
+ origins in your project settings) and call `logger.start()` to resume.
283
+
284
+ ## Environment variables
285
+
286
+ | Variable | Purpose |
287
+ | -------------------- | ---------------------------------------------------- |
288
+ | `VEDATRACE_API_KEY` | Used when `apiKey` is not passed to `vedatrace()`. |
289
+
290
+ Reading it is guarded, so it is safe in browsers and in Workers without
291
+ `nodejs_compat` - those simply see no key and the logger stays inert.
292
+
293
+ ## Browser usage
294
+
295
+ In the browser the SDK batches and flushes on `visibilitychange` and `pagehide`,
296
+ using `keepalive` so the final request outlives the page. It deliberately does
297
+ **not** register `beforeunload` or `unload`, either of which would make the host
298
+ page ineligible for the back/forward cache.
299
+
300
+ > **Your API key ships in the bundle.** Anyone can read it out of your JavaScript.
301
+ > Restrict browser keys to your own origins under **Project settings -> API keys ->
302
+ > Allowed origins**, and use a separate, unrestricted key for your backend. A key
303
+ > with no origin list accepts logs from anywhere.
304
+
265
305
  ## Log Schema
266
306
 
267
307
  Logs sent to VedaTrace follow this schema:
@@ -328,6 +368,12 @@ Get the detected runtime environment. Returns: `'node' | 'browser' | 'cloudflare
328
368
  console.log(logger.runtime) // 'cloudflare' when running in Workers
329
369
  ```
330
370
 
371
+ ## Contributing
372
+
373
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). Note that commit messages drive the
374
+ published version — `feat:` is a minor, `fix:` a patch, `refactor:` releases
375
+ nothing.
376
+
331
377
  ## License
332
378
 
333
379
  MIT
@@ -0,0 +1,231 @@
1
+ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
2
+ const FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
3
+ class VedaTraceTransportError extends Error {
4
+ name = "VedaTraceTransportError";
5
+ status;
6
+ retryable;
7
+ fatal;
8
+ /** Server-requested backoff from a `Retry-After` header, in milliseconds. */
9
+ retryAfterMs;
10
+ constructor(message, options = {}) {
11
+ super(message);
12
+ this.status = options.status;
13
+ this.fatal = options.fatal ?? false;
14
+ this.retryable = options.retryable ?? true;
15
+ this.retryAfterMs = options.retryAfterMs;
16
+ }
17
+ /** Build an error from an HTTP response status. */
18
+ static fromStatus(status, body, retryAfterMs2) {
19
+ const fatal = FATAL_STATUSES.has(status);
20
+ const retryable = status >= 500 || RETRYABLE_STATUSES.has(status);
21
+ return new VedaTraceTransportError(`HTTP ${status}: ${body}`, {
22
+ status,
23
+ retryable,
24
+ fatal,
25
+ retryAfterMs: retryAfterMs2
26
+ });
27
+ }
28
+ }
29
+ function isRetryable(error) {
30
+ if (error instanceof VedaTraceTransportError) return error.retryable;
31
+ return true;
32
+ }
33
+ function isFatal(error) {
34
+ return error instanceof VedaTraceTransportError && error.fatal;
35
+ }
36
+ function retryAfterMs(errors) {
37
+ let longest;
38
+ for (const error of errors) {
39
+ if (error instanceof VedaTraceTransportError && error.retryAfterMs) {
40
+ longest = Math.max(longest ?? 0, error.retryAfterMs);
41
+ }
42
+ }
43
+ return longest;
44
+ }
45
+ function parseRetryAfter(header) {
46
+ if (!header) return void 0;
47
+ const seconds = Number(header);
48
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
49
+ const date = Date.parse(header);
50
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
51
+ return void 0;
52
+ }
53
+
54
+ const LEVEL_COLORS = {
55
+ debug: "\x1B[36m",
56
+ // cyan
57
+ info: "\x1B[32m",
58
+ // green
59
+ warn: "\x1B[33m",
60
+ // yellow
61
+ error: "\x1B[31m",
62
+ // red
63
+ fatal: "\x1B[35m"
64
+ // magenta
65
+ };
66
+ const RESET_COLOR = "\x1B[0m";
67
+ const LEVEL_PRIORITY = {
68
+ debug: 0,
69
+ info: 1,
70
+ warn: 2,
71
+ error: 3,
72
+ fatal: 4
73
+ };
74
+ class VedaTraceConsoleTransport {
75
+ name = "console";
76
+ format;
77
+ colors;
78
+ minLevel;
79
+ constructor(config = {}) {
80
+ this.format = config.format ?? "pretty";
81
+ this.colors = config.colors ?? true;
82
+ this.minLevel = config.minLevel ?? "debug";
83
+ }
84
+ /** Send logs to console */
85
+ send(logs) {
86
+ for (const log of logs) {
87
+ if (LEVEL_PRIORITY[log.level] < LEVEL_PRIORITY[this.minLevel]) {
88
+ continue;
89
+ }
90
+ switch (this.format) {
91
+ case "json":
92
+ this.logJson(log);
93
+ break;
94
+ case "simple":
95
+ this.logSimple(log);
96
+ break;
97
+ default:
98
+ this.logPretty(log);
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ /** Format as JSON */
104
+ logJson(log) {
105
+ console.log(JSON.stringify(log));
106
+ }
107
+ /** Format as simple text */
108
+ logSimple(log) {
109
+ const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
110
+ const service = log.service ? `[${log.service}] ` : "";
111
+ console.log(
112
+ `${timestamp} ${log.level.toUpperCase()} ${service}${log.message}`
113
+ );
114
+ }
115
+ /** Format as pretty colored output */
116
+ logPretty(log) {
117
+ const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
118
+ const color = this.colors ? LEVEL_COLORS[log.level] : "";
119
+ const reset = this.colors ? RESET_COLOR : "";
120
+ const service = log.service ? ` ${color}[${log.service}]${reset}` : "";
121
+ let output = `${timestamp} ${color}${log.level.toUpperCase()}${reset}${service} ${log.message}`;
122
+ if (log.metadata && Object.keys(log.metadata).length > 0) {
123
+ output += `
124
+ ${color}metadata:${reset} ${JSON.stringify(log.metadata, null, 2).replace(/\n/g, "\n ")}`;
125
+ }
126
+ console.log(output);
127
+ }
128
+ }
129
+
130
+ const KEEPALIVE_MAX_BYTES = 56 * 1024;
131
+ const encoder = new TextEncoder();
132
+ class VedaTraceHttpTransport {
133
+ name = "http";
134
+ endpoint;
135
+ apiKey;
136
+ timeout;
137
+ headers;
138
+ keepalive;
139
+ constructor(config) {
140
+ this.apiKey = config.apiKey;
141
+ this.endpoint = config.endpoint ?? "https://ingest.vedatrace.dev/v1/logs";
142
+ this.timeout = config.timeout ?? 3e4;
143
+ this.headers = config.headers ?? {};
144
+ this.keepalive = config.keepalive ?? false;
145
+ }
146
+ /** Send logs via HTTP POST */
147
+ async send(logs) {
148
+ if (logs.length === 0) return;
149
+ const body = JSON.stringify(logs.map(toWirePayload));
150
+ if (this.keepalive && encoder.encode(body).length > KEEPALIVE_MAX_BYTES) {
151
+ if (logs.length > 1) {
152
+ const mid = Math.ceil(logs.length / 2);
153
+ await this.send(logs.slice(0, mid));
154
+ await this.send(logs.slice(mid));
155
+ return;
156
+ }
157
+ return this.post(body, false);
158
+ }
159
+ return this.post(body, this.keepalive);
160
+ }
161
+ async post(body, keepalive) {
162
+ const controller = new AbortController();
163
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
164
+ try {
165
+ const response = await fetch(this.endpoint, {
166
+ method: "POST",
167
+ headers: {
168
+ "Content-Type": "application/json",
169
+ "X-API-Key": this.apiKey,
170
+ ...this.headers
171
+ },
172
+ body,
173
+ signal: controller.signal,
174
+ keepalive
175
+ });
176
+ if (!response.ok) {
177
+ const text = await response.text().catch(() => "");
178
+ throw VedaTraceTransportError.fromStatus(
179
+ response.status,
180
+ text,
181
+ parseRetryAfter(response.headers.get("Retry-After"))
182
+ );
183
+ }
184
+ } catch (error) {
185
+ if (error instanceof VedaTraceTransportError) throw error;
186
+ if (error instanceof Error && error.name === "AbortError") {
187
+ throw new VedaTraceTransportError(
188
+ `Request timeout after ${this.timeout}ms`,
189
+ { retryable: true }
190
+ );
191
+ }
192
+ throw new VedaTraceTransportError(
193
+ error instanceof Error ? error.message : String(error),
194
+ { retryable: true }
195
+ );
196
+ } finally {
197
+ clearTimeout(timeoutId);
198
+ }
199
+ }
200
+ /**
201
+ * No-op: this transport holds no buffer of its own. The batcher owns the
202
+ * queue and calls send() directly, including on the browser's final flush.
203
+ */
204
+ async flush() {
205
+ return Promise.resolve();
206
+ }
207
+ /** Check if keepalive is enabled */
208
+ isKeepaliveEnabled() {
209
+ return this.keepalive;
210
+ }
211
+ /** Enable/disable keepalive */
212
+ setKeepalive(enabled) {
213
+ this.keepalive = enabled;
214
+ }
215
+ }
216
+ function toWirePayload(log) {
217
+ return {
218
+ level: log.level,
219
+ message: log.message,
220
+ service: log.service,
221
+ timestamp: log.timestamp ? new Date(log.timestamp).toISOString() : void 0,
222
+ metadata: log.metadata
223
+ };
224
+ }
225
+ class VedaTraceHttpTransportBrowser extends VedaTraceHttpTransport {
226
+ constructor(config) {
227
+ super({ ...config, keepalive: true });
228
+ }
229
+ }
230
+
231
+ export { VedaTraceHttpTransportBrowser as V, isRetryable as a, VedaTraceHttpTransport as b, VedaTraceConsoleTransport as c, VedaTraceTransportError as d, isFatal as i, retryAfterMs as r };
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
4
+ const FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
5
+ class VedaTraceTransportError extends Error {
6
+ name = "VedaTraceTransportError";
7
+ status;
8
+ retryable;
9
+ fatal;
10
+ /** Server-requested backoff from a `Retry-After` header, in milliseconds. */
11
+ retryAfterMs;
12
+ constructor(message, options = {}) {
13
+ super(message);
14
+ this.status = options.status;
15
+ this.fatal = options.fatal ?? false;
16
+ this.retryable = options.retryable ?? true;
17
+ this.retryAfterMs = options.retryAfterMs;
18
+ }
19
+ /** Build an error from an HTTP response status. */
20
+ static fromStatus(status, body, retryAfterMs2) {
21
+ const fatal = FATAL_STATUSES.has(status);
22
+ const retryable = status >= 500 || RETRYABLE_STATUSES.has(status);
23
+ return new VedaTraceTransportError(`HTTP ${status}: ${body}`, {
24
+ status,
25
+ retryable,
26
+ fatal,
27
+ retryAfterMs: retryAfterMs2
28
+ });
29
+ }
30
+ }
31
+ function isRetryable(error) {
32
+ if (error instanceof VedaTraceTransportError) return error.retryable;
33
+ return true;
34
+ }
35
+ function isFatal(error) {
36
+ return error instanceof VedaTraceTransportError && error.fatal;
37
+ }
38
+ function retryAfterMs(errors) {
39
+ let longest;
40
+ for (const error of errors) {
41
+ if (error instanceof VedaTraceTransportError && error.retryAfterMs) {
42
+ longest = Math.max(longest ?? 0, error.retryAfterMs);
43
+ }
44
+ }
45
+ return longest;
46
+ }
47
+ function parseRetryAfter(header) {
48
+ if (!header) return void 0;
49
+ const seconds = Number(header);
50
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
51
+ const date = Date.parse(header);
52
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
53
+ return void 0;
54
+ }
55
+
56
+ const LEVEL_COLORS = {
57
+ debug: "\x1B[36m",
58
+ // cyan
59
+ info: "\x1B[32m",
60
+ // green
61
+ warn: "\x1B[33m",
62
+ // yellow
63
+ error: "\x1B[31m",
64
+ // red
65
+ fatal: "\x1B[35m"
66
+ // magenta
67
+ };
68
+ const RESET_COLOR = "\x1B[0m";
69
+ const LEVEL_PRIORITY = {
70
+ debug: 0,
71
+ info: 1,
72
+ warn: 2,
73
+ error: 3,
74
+ fatal: 4
75
+ };
76
+ class VedaTraceConsoleTransport {
77
+ name = "console";
78
+ format;
79
+ colors;
80
+ minLevel;
81
+ constructor(config = {}) {
82
+ this.format = config.format ?? "pretty";
83
+ this.colors = config.colors ?? true;
84
+ this.minLevel = config.minLevel ?? "debug";
85
+ }
86
+ /** Send logs to console */
87
+ send(logs) {
88
+ for (const log of logs) {
89
+ if (LEVEL_PRIORITY[log.level] < LEVEL_PRIORITY[this.minLevel]) {
90
+ continue;
91
+ }
92
+ switch (this.format) {
93
+ case "json":
94
+ this.logJson(log);
95
+ break;
96
+ case "simple":
97
+ this.logSimple(log);
98
+ break;
99
+ default:
100
+ this.logPretty(log);
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ /** Format as JSON */
106
+ logJson(log) {
107
+ console.log(JSON.stringify(log));
108
+ }
109
+ /** Format as simple text */
110
+ logSimple(log) {
111
+ const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
112
+ const service = log.service ? `[${log.service}] ` : "";
113
+ console.log(
114
+ `${timestamp} ${log.level.toUpperCase()} ${service}${log.message}`
115
+ );
116
+ }
117
+ /** Format as pretty colored output */
118
+ logPretty(log) {
119
+ const timestamp = new Date(log.timestamp ?? Date.now()).toISOString();
120
+ const color = this.colors ? LEVEL_COLORS[log.level] : "";
121
+ const reset = this.colors ? RESET_COLOR : "";
122
+ const service = log.service ? ` ${color}[${log.service}]${reset}` : "";
123
+ let output = `${timestamp} ${color}${log.level.toUpperCase()}${reset}${service} ${log.message}`;
124
+ if (log.metadata && Object.keys(log.metadata).length > 0) {
125
+ output += `
126
+ ${color}metadata:${reset} ${JSON.stringify(log.metadata, null, 2).replace(/\n/g, "\n ")}`;
127
+ }
128
+ console.log(output);
129
+ }
130
+ }
131
+
132
+ const KEEPALIVE_MAX_BYTES = 56 * 1024;
133
+ const encoder = new TextEncoder();
134
+ class VedaTraceHttpTransport {
135
+ name = "http";
136
+ endpoint;
137
+ apiKey;
138
+ timeout;
139
+ headers;
140
+ keepalive;
141
+ constructor(config) {
142
+ this.apiKey = config.apiKey;
143
+ this.endpoint = config.endpoint ?? "https://ingest.vedatrace.dev/v1/logs";
144
+ this.timeout = config.timeout ?? 3e4;
145
+ this.headers = config.headers ?? {};
146
+ this.keepalive = config.keepalive ?? false;
147
+ }
148
+ /** Send logs via HTTP POST */
149
+ async send(logs) {
150
+ if (logs.length === 0) return;
151
+ const body = JSON.stringify(logs.map(toWirePayload));
152
+ if (this.keepalive && encoder.encode(body).length > KEEPALIVE_MAX_BYTES) {
153
+ if (logs.length > 1) {
154
+ const mid = Math.ceil(logs.length / 2);
155
+ await this.send(logs.slice(0, mid));
156
+ await this.send(logs.slice(mid));
157
+ return;
158
+ }
159
+ return this.post(body, false);
160
+ }
161
+ return this.post(body, this.keepalive);
162
+ }
163
+ async post(body, keepalive) {
164
+ const controller = new AbortController();
165
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
166
+ try {
167
+ const response = await fetch(this.endpoint, {
168
+ method: "POST",
169
+ headers: {
170
+ "Content-Type": "application/json",
171
+ "X-API-Key": this.apiKey,
172
+ ...this.headers
173
+ },
174
+ body,
175
+ signal: controller.signal,
176
+ keepalive
177
+ });
178
+ if (!response.ok) {
179
+ const text = await response.text().catch(() => "");
180
+ throw VedaTraceTransportError.fromStatus(
181
+ response.status,
182
+ text,
183
+ parseRetryAfter(response.headers.get("Retry-After"))
184
+ );
185
+ }
186
+ } catch (error) {
187
+ if (error instanceof VedaTraceTransportError) throw error;
188
+ if (error instanceof Error && error.name === "AbortError") {
189
+ throw new VedaTraceTransportError(
190
+ `Request timeout after ${this.timeout}ms`,
191
+ { retryable: true }
192
+ );
193
+ }
194
+ throw new VedaTraceTransportError(
195
+ error instanceof Error ? error.message : String(error),
196
+ { retryable: true }
197
+ );
198
+ } finally {
199
+ clearTimeout(timeoutId);
200
+ }
201
+ }
202
+ /**
203
+ * No-op: this transport holds no buffer of its own. The batcher owns the
204
+ * queue and calls send() directly, including on the browser's final flush.
205
+ */
206
+ async flush() {
207
+ return Promise.resolve();
208
+ }
209
+ /** Check if keepalive is enabled */
210
+ isKeepaliveEnabled() {
211
+ return this.keepalive;
212
+ }
213
+ /** Enable/disable keepalive */
214
+ setKeepalive(enabled) {
215
+ this.keepalive = enabled;
216
+ }
217
+ }
218
+ function toWirePayload(log) {
219
+ return {
220
+ level: log.level,
221
+ message: log.message,
222
+ service: log.service,
223
+ timestamp: log.timestamp ? new Date(log.timestamp).toISOString() : void 0,
224
+ metadata: log.metadata
225
+ };
226
+ }
227
+ class VedaTraceHttpTransportBrowser extends VedaTraceHttpTransport {
228
+ constructor(config) {
229
+ super({ ...config, keepalive: true });
230
+ }
231
+ }
232
+
233
+ exports.VedaTraceConsoleTransport = VedaTraceConsoleTransport;
234
+ exports.VedaTraceHttpTransport = VedaTraceHttpTransport;
235
+ exports.VedaTraceHttpTransportBrowser = VedaTraceHttpTransportBrowser;
236
+ exports.VedaTraceTransportError = VedaTraceTransportError;
237
+ exports.isFatal = isFatal;
238
+ exports.isRetryable = isRetryable;
239
+ exports.retryAfterMs = retryAfterMs;