@parsrun/core 0.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.
@@ -0,0 +1,657 @@
1
+ // src/runtime.ts
2
+ function detectRuntime() {
3
+ if (typeof globalThis !== "undefined" && "Bun" in globalThis) {
4
+ return "bun";
5
+ }
6
+ if (typeof globalThis !== "undefined" && "Deno" in globalThis) {
7
+ return "deno";
8
+ }
9
+ if (typeof globalThis !== "undefined" && typeof globalThis.caches !== "undefined" && typeof globalThis.process === "undefined") {
10
+ return "cloudflare";
11
+ }
12
+ if (typeof globalThis !== "undefined" && typeof globalThis.EdgeRuntime !== "undefined") {
13
+ return "edge";
14
+ }
15
+ if (typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined") {
16
+ return "browser";
17
+ }
18
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
19
+ return "node";
20
+ }
21
+ return "unknown";
22
+ }
23
+ var runtime = detectRuntime();
24
+ var runtimeInfo = {
25
+ runtime,
26
+ isNode: runtime === "node",
27
+ isDeno: runtime === "deno",
28
+ isBun: runtime === "bun",
29
+ isCloudflare: runtime === "cloudflare",
30
+ isEdge: runtime === "cloudflare" || runtime === "edge" || runtime === "deno",
31
+ isBrowser: runtime === "browser",
32
+ isServer: runtime !== "browser",
33
+ supportsWebCrypto: typeof globalThis.crypto?.subtle !== "undefined",
34
+ supportsStreams: typeof globalThis.ReadableStream !== "undefined"
35
+ };
36
+
37
+ // src/env.ts
38
+ var edgeEnvStore = {};
39
+ function getEnv(key, defaultValue) {
40
+ if (runtime === "cloudflare" || runtime === "edge") {
41
+ return edgeEnvStore[key] ?? defaultValue;
42
+ }
43
+ if (runtime === "deno") {
44
+ try {
45
+ return globalThis.Deno.env.get(key) ?? defaultValue;
46
+ } catch {
47
+ return defaultValue;
48
+ }
49
+ }
50
+ if (typeof process !== "undefined" && process.env) {
51
+ return process.env[key] ?? defaultValue;
52
+ }
53
+ if (runtime === "browser" && typeof globalThis.__ENV__ !== "undefined") {
54
+ return globalThis.__ENV__[key] ?? defaultValue;
55
+ }
56
+ return defaultValue;
57
+ }
58
+ function isDevelopment() {
59
+ const env = getEnv("NODE_ENV");
60
+ return env === "development" || env === void 0;
61
+ }
62
+
63
+ // src/transports/console.ts
64
+ var ConsoleTransport = class {
65
+ name = "console";
66
+ pretty;
67
+ colors;
68
+ constructor(options = {}) {
69
+ this.pretty = options.pretty ?? isDevelopment();
70
+ this.colors = options.colors ?? (runtime === "node" || runtime === "bun");
71
+ }
72
+ log(entry) {
73
+ if (this.pretty) {
74
+ this.logPretty(entry);
75
+ } else {
76
+ this.logJson(entry);
77
+ }
78
+ }
79
+ logJson(entry) {
80
+ const { level, message, timestamp, context, error } = entry;
81
+ const output = {
82
+ level,
83
+ time: timestamp,
84
+ msg: message
85
+ };
86
+ if (context && Object.keys(context).length > 0) {
87
+ Object.assign(output, context);
88
+ }
89
+ if (error) {
90
+ output["err"] = error;
91
+ }
92
+ console.log(JSON.stringify(output));
93
+ }
94
+ logPretty(entry) {
95
+ const { level, message, timestamp, context, error } = entry;
96
+ const levelColors = {
97
+ TRACE: "\x1B[90m",
98
+ // Gray
99
+ DEBUG: "\x1B[36m",
100
+ // Cyan
101
+ INFO: "\x1B[32m",
102
+ // Green
103
+ WARN: "\x1B[33m",
104
+ // Yellow
105
+ ERROR: "\x1B[31m",
106
+ // Red
107
+ FATAL: "\x1B[35m",
108
+ // Magenta
109
+ SILENT: ""
110
+ };
111
+ const reset = "\x1B[0m";
112
+ const color = this.colors ? levelColors[level] : "";
113
+ const resetCode = this.colors ? reset : "";
114
+ const timePart = timestamp.split("T")[1];
115
+ const time = timePart ? timePart.slice(0, 8) : timestamp;
116
+ let output = `${color}[${time}] ${level.padEnd(5)}${resetCode} ${message}`;
117
+ if (context && Object.keys(context).length > 0) {
118
+ output += ` ${JSON.stringify(context)}`;
119
+ }
120
+ if (level === "ERROR" || level === "FATAL") {
121
+ console.error(output);
122
+ if (error?.stack) {
123
+ console.error(error.stack);
124
+ }
125
+ } else if (level === "WARN") {
126
+ console.warn(output);
127
+ } else if (level === "DEBUG" || level === "TRACE") {
128
+ console.debug(output);
129
+ } else {
130
+ console.log(output);
131
+ }
132
+ }
133
+ };
134
+
135
+ // src/transports/axiom.ts
136
+ var AxiomTransport = class {
137
+ name = "axiom";
138
+ buffer = [];
139
+ flushTimer = null;
140
+ isFlushing = false;
141
+ options;
142
+ constructor(options) {
143
+ this.options = {
144
+ batchSize: 100,
145
+ flushInterval: 5e3,
146
+ apiUrl: "https://api.axiom.co",
147
+ ...options
148
+ };
149
+ if (this.options.flushInterval > 0) {
150
+ this.flushTimer = setInterval(
151
+ () => this.flush(),
152
+ this.options.flushInterval
153
+ );
154
+ }
155
+ }
156
+ log(entry) {
157
+ if (this.options.enabled === false) return;
158
+ const event = {
159
+ _time: entry.timestamp,
160
+ level: entry.level,
161
+ message: entry.message
162
+ };
163
+ if (entry.context) {
164
+ Object.assign(event, entry.context);
165
+ }
166
+ if (entry.error) {
167
+ event["error.name"] = entry.error.name;
168
+ event["error.message"] = entry.error.message;
169
+ event["error.stack"] = entry.error.stack;
170
+ }
171
+ this.buffer.push(event);
172
+ if (this.buffer.length >= this.options.batchSize) {
173
+ this.flush();
174
+ }
175
+ }
176
+ async flush() {
177
+ if (this.isFlushing || this.buffer.length === 0) return;
178
+ this.isFlushing = true;
179
+ const events = this.buffer;
180
+ this.buffer = [];
181
+ try {
182
+ const response = await fetch(
183
+ `${this.options.apiUrl}/v1/datasets/${this.options.dataset}/ingest`,
184
+ {
185
+ method: "POST",
186
+ headers: {
187
+ Authorization: `Bearer ${this.options.token}`,
188
+ "Content-Type": "application/json",
189
+ ...this.options.orgId && {
190
+ "X-Axiom-Org-Id": this.options.orgId
191
+ }
192
+ },
193
+ body: JSON.stringify(events)
194
+ }
195
+ );
196
+ if (!response.ok) {
197
+ const errorText = await response.text();
198
+ throw new Error(`Axiom ingest failed: ${response.status} ${errorText}`);
199
+ }
200
+ } catch (error) {
201
+ if (this.options.onError) {
202
+ this.options.onError(
203
+ error instanceof Error ? error : new Error(String(error)),
204
+ events.length
205
+ );
206
+ } else {
207
+ console.error("[Axiom] Failed to send logs:", error);
208
+ }
209
+ } finally {
210
+ this.isFlushing = false;
211
+ }
212
+ }
213
+ async close() {
214
+ if (this.flushTimer) {
215
+ clearInterval(this.flushTimer);
216
+ this.flushTimer = null;
217
+ }
218
+ await this.flush();
219
+ }
220
+ };
221
+ function createAxiomTransport(options) {
222
+ return new AxiomTransport(options);
223
+ }
224
+
225
+ // src/transports/sentry.ts
226
+ var SentryTransport = class {
227
+ name = "sentry";
228
+ client;
229
+ dsn;
230
+ options;
231
+ user = null;
232
+ contexts = /* @__PURE__ */ new Map();
233
+ breadcrumbs = [];
234
+ maxBreadcrumbs = 100;
235
+ constructor(options) {
236
+ this.options = {
237
+ sampleRate: 1,
238
+ ...options
239
+ };
240
+ if (options.client) {
241
+ this.client = options.client;
242
+ } else if (options.dsn) {
243
+ this.dsn = this.parseDSN(options.dsn);
244
+ } else {
245
+ throw new Error("SentryTransport requires either 'dsn' or 'client' option");
246
+ }
247
+ }
248
+ /**
249
+ * Parse Sentry DSN
250
+ */
251
+ parseDSN(dsn) {
252
+ const match = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);
253
+ if (!match || !match[1] || !match[2] || !match[3] || !match[4]) {
254
+ throw new Error(`Invalid Sentry DSN: ${dsn}`);
255
+ }
256
+ return {
257
+ protocol: match[1],
258
+ publicKey: match[2],
259
+ host: match[3],
260
+ projectId: match[4]
261
+ };
262
+ }
263
+ /**
264
+ * LogTransport implementation
265
+ * Only sends ERROR and FATAL level logs
266
+ */
267
+ log(entry) {
268
+ if (this.options.enabled === false) return;
269
+ if (entry.levelValue < 50) return;
270
+ if (entry.error) {
271
+ const error = new Error(entry.error.message);
272
+ error.name = entry.error.name;
273
+ if (entry.error.stack) {
274
+ error.stack = entry.error.stack;
275
+ }
276
+ this.captureException(
277
+ error,
278
+ entry.context ? { extra: entry.context } : void 0
279
+ );
280
+ } else {
281
+ this.captureMessage(
282
+ entry.message,
283
+ entry.level === "FATAL" ? "error" : "warning",
284
+ entry.context ? { extra: entry.context } : void 0
285
+ );
286
+ }
287
+ }
288
+ /**
289
+ * Capture an exception
290
+ */
291
+ captureException(error, context) {
292
+ if (this.options.enabled === false) return;
293
+ if (!this.shouldSample()) return;
294
+ if (this.client) {
295
+ this.captureWithSdk(error, context);
296
+ } else {
297
+ this.captureWithHttp(error, context);
298
+ }
299
+ }
300
+ /**
301
+ * Capture a message
302
+ */
303
+ captureMessage(message, level, context) {
304
+ if (this.options.enabled === false) return;
305
+ if (!this.shouldSample()) return;
306
+ if (this.client) {
307
+ this.client.withScope((scope) => {
308
+ this.applyContext(scope, context);
309
+ scope.setLevel(level);
310
+ this.client.captureMessage(message, level);
311
+ });
312
+ } else {
313
+ this.sendHttpEvent({
314
+ level: level === "warning" ? "warning" : level === "info" ? "info" : "error",
315
+ message: { formatted: message },
316
+ ...this.buildEventContext(context)
317
+ });
318
+ }
319
+ }
320
+ /**
321
+ * Set user context
322
+ */
323
+ setUser(user) {
324
+ this.user = user;
325
+ }
326
+ /**
327
+ * Set custom context
328
+ */
329
+ setContext(name, context) {
330
+ this.contexts.set(name, context);
331
+ }
332
+ /**
333
+ * Add breadcrumb
334
+ */
335
+ addBreadcrumb(breadcrumb) {
336
+ this.breadcrumbs.push({
337
+ ...breadcrumb,
338
+ timestamp: breadcrumb.timestamp ?? Date.now() / 1e3
339
+ });
340
+ if (this.breadcrumbs.length > this.maxBreadcrumbs) {
341
+ this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
342
+ }
343
+ }
344
+ /**
345
+ * Flush pending events
346
+ */
347
+ async flush() {
348
+ if (this.client?.flush) {
349
+ await this.client.flush(2e3);
350
+ }
351
+ }
352
+ // ============================================================================
353
+ // Private Methods
354
+ // ============================================================================
355
+ shouldSample() {
356
+ const rate = this.options.sampleRate ?? 1;
357
+ return Math.random() < rate;
358
+ }
359
+ /**
360
+ * Capture with SDK (BYOS mode)
361
+ */
362
+ captureWithSdk(error, context) {
363
+ this.client.withScope((scope) => {
364
+ this.applyContext(scope, context);
365
+ this.client.captureException(error);
366
+ });
367
+ }
368
+ /**
369
+ * Apply context to SDK scope
370
+ */
371
+ applyContext(scope, context) {
372
+ if (this.user) {
373
+ scope.setUser(this.user);
374
+ } else if (context?.userId) {
375
+ scope.setUser({ id: context.userId });
376
+ }
377
+ if (this.options.tags) {
378
+ for (const [key, value] of Object.entries(this.options.tags)) {
379
+ scope.setTag(key, value);
380
+ }
381
+ }
382
+ if (context?.tags) {
383
+ for (const [key, value] of Object.entries(context.tags)) {
384
+ scope.setTag(key, value);
385
+ }
386
+ }
387
+ if (context?.requestId) {
388
+ scope.setTag("requestId", context.requestId);
389
+ }
390
+ if (context?.tenantId) {
391
+ scope.setTag("tenantId", context.tenantId);
392
+ }
393
+ if (context?.extra) {
394
+ scope.setExtras(context.extra);
395
+ }
396
+ for (const bc of this.breadcrumbs) {
397
+ scope.addBreadcrumb(bc);
398
+ }
399
+ }
400
+ /**
401
+ * Capture with HTTP API (default mode)
402
+ */
403
+ captureWithHttp(error, context) {
404
+ const stacktrace = this.parseStackTrace(error.stack);
405
+ const exceptionValue = {
406
+ type: error.name,
407
+ value: error.message
408
+ };
409
+ if (stacktrace) {
410
+ exceptionValue.stacktrace = stacktrace;
411
+ }
412
+ const event = {
413
+ level: "error",
414
+ exception: {
415
+ values: [exceptionValue]
416
+ },
417
+ ...this.buildEventContext(context)
418
+ };
419
+ this.sendHttpEvent(event);
420
+ }
421
+ /**
422
+ * Build event context for HTTP API
423
+ */
424
+ buildEventContext(context) {
425
+ const event = {};
426
+ if (this.options.environment) {
427
+ event.environment = this.options.environment;
428
+ }
429
+ if (this.options.release) {
430
+ event.release = this.options.release;
431
+ }
432
+ if (this.options.serverName) {
433
+ event.server_name = this.options.serverName;
434
+ }
435
+ const tags = { ...this.options.tags };
436
+ if (context?.tags) {
437
+ Object.assign(tags, context.tags);
438
+ }
439
+ if (context?.requestId) {
440
+ tags["requestId"] = context.requestId;
441
+ }
442
+ if (context?.tenantId) {
443
+ tags["tenantId"] = context.tenantId;
444
+ }
445
+ if (Object.keys(tags).length > 0) {
446
+ event.tags = tags;
447
+ }
448
+ if (context?.extra) {
449
+ event.extra = context.extra;
450
+ }
451
+ if (this.user) {
452
+ event.user = this.user;
453
+ } else if (context?.userId) {
454
+ event.user = { id: context.userId };
455
+ }
456
+ if (this.breadcrumbs.length > 0) {
457
+ event.breadcrumbs = this.breadcrumbs.map((bc) => {
458
+ const crumb = {};
459
+ if (bc.type) crumb.type = bc.type;
460
+ if (bc.category) crumb.category = bc.category;
461
+ if (bc.message) crumb.message = bc.message;
462
+ if (bc.data) crumb.data = bc.data;
463
+ if (bc.level) crumb.level = bc.level;
464
+ if (bc.timestamp !== void 0) crumb.timestamp = bc.timestamp;
465
+ return crumb;
466
+ });
467
+ }
468
+ if (this.contexts.size > 0) {
469
+ event.contexts = Object.fromEntries(this.contexts);
470
+ }
471
+ return event;
472
+ }
473
+ /**
474
+ * Parse error stack trace into Sentry format
475
+ */
476
+ parseStackTrace(stack) {
477
+ if (!stack) return void 0;
478
+ const lines = stack.split("\n").slice(1);
479
+ const frames = [];
480
+ for (const line of lines) {
481
+ const match = line.match(/^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
482
+ if (match && match[3] && match[4]) {
483
+ const frame = {
484
+ function: match[1] || "<anonymous>",
485
+ lineno: parseInt(match[3], 10),
486
+ colno: parseInt(match[4], 10)
487
+ };
488
+ if (match[2]) {
489
+ frame.filename = match[2];
490
+ }
491
+ frames.push(frame);
492
+ }
493
+ }
494
+ frames.reverse();
495
+ return frames.length > 0 ? { frames } : void 0;
496
+ }
497
+ /**
498
+ * Generate event ID
499
+ */
500
+ generateEventId() {
501
+ const bytes = new Uint8Array(16);
502
+ crypto.getRandomValues(bytes);
503
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
504
+ }
505
+ /**
506
+ * Send event via HTTP API
507
+ */
508
+ async sendHttpEvent(eventData) {
509
+ if (!this.dsn) return;
510
+ const event = {
511
+ event_id: this.generateEventId(),
512
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
513
+ platform: "javascript",
514
+ level: "error",
515
+ ...eventData
516
+ };
517
+ if (this.options.beforeSend) {
518
+ const result = this.options.beforeSend(event);
519
+ if (result === null) return;
520
+ }
521
+ const url = `${this.dsn.protocol}://${this.dsn.host}/api/${this.dsn.projectId}/store/`;
522
+ try {
523
+ const response = await fetch(url, {
524
+ method: "POST",
525
+ headers: {
526
+ "Content-Type": "application/json",
527
+ "X-Sentry-Auth": [
528
+ "Sentry sentry_version=7",
529
+ `sentry_client=pars-sentry/1.0.0`,
530
+ `sentry_key=${this.dsn.publicKey}`
531
+ ].join(", ")
532
+ },
533
+ body: JSON.stringify(event)
534
+ });
535
+ if (!response.ok) {
536
+ throw new Error(`Sentry API error: ${response.status}`);
537
+ }
538
+ } catch (error) {
539
+ if (this.options.onError) {
540
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
541
+ }
542
+ }
543
+ }
544
+ };
545
+ function createSentryTransport(options) {
546
+ return new SentryTransport(options);
547
+ }
548
+
549
+ // src/transports/logtape.ts
550
+ var FallbackLogger = class {
551
+ constructor(category) {
552
+ this.category = category;
553
+ }
554
+ log(level, message, properties) {
555
+ const entry = {
556
+ level,
557
+ category: this.category,
558
+ msg: message,
559
+ time: (/* @__PURE__ */ new Date()).toISOString(),
560
+ ...properties
561
+ };
562
+ console.log(JSON.stringify(entry));
563
+ }
564
+ debug(message, properties) {
565
+ this.log("debug", message, properties);
566
+ }
567
+ info(message, properties) {
568
+ this.log("info", message, properties);
569
+ }
570
+ warn(message, properties) {
571
+ this.log("warn", message, properties);
572
+ }
573
+ warning(message, properties) {
574
+ this.log("warning", message, properties);
575
+ }
576
+ error(message, properties) {
577
+ this.log("error", message, properties);
578
+ }
579
+ fatal(message, properties) {
580
+ this.log("fatal", message, properties);
581
+ }
582
+ };
583
+ var LogtapeTransport = class {
584
+ name = "logtape";
585
+ logger;
586
+ includeTimestamp;
587
+ includeLevelValue;
588
+ enabled;
589
+ constructor(options = {}) {
590
+ this.enabled = options.enabled !== false;
591
+ this.includeTimestamp = options.includeTimestamp !== false;
592
+ this.includeLevelValue = options.includeLevelValue ?? false;
593
+ if (options.logger) {
594
+ this.logger = options.logger;
595
+ } else {
596
+ this.logger = new FallbackLogger(options.category ?? "pars");
597
+ }
598
+ }
599
+ log(entry) {
600
+ if (!this.enabled) return;
601
+ const level = this.mapLevel(entry.level);
602
+ const properties = this.buildProperties(entry);
603
+ this.logger[level](entry.message, properties);
604
+ }
605
+ /**
606
+ * Map Pars log level to Logtape level
607
+ */
608
+ mapLevel(level) {
609
+ const mapping = {
610
+ TRACE: "debug",
611
+ DEBUG: "debug",
612
+ INFO: "info",
613
+ WARN: "warning",
614
+ ERROR: "error",
615
+ FATAL: "fatal",
616
+ SILENT: "debug"
617
+ // Should never be logged
618
+ };
619
+ return mapping[level];
620
+ }
621
+ /**
622
+ * Build properties object for Logtape
623
+ */
624
+ buildProperties(entry) {
625
+ const properties = {};
626
+ if (this.includeTimestamp) {
627
+ properties["timestamp"] = entry.timestamp;
628
+ }
629
+ if (this.includeLevelValue) {
630
+ properties["levelValue"] = entry.levelValue;
631
+ }
632
+ if (entry.context) {
633
+ Object.assign(properties, entry.context);
634
+ }
635
+ if (entry.error) {
636
+ properties["error"] = {
637
+ name: entry.error.name,
638
+ message: entry.error.message,
639
+ stack: entry.error.stack
640
+ };
641
+ }
642
+ return properties;
643
+ }
644
+ };
645
+ function createLogtapeTransport(options) {
646
+ return new LogtapeTransport(options);
647
+ }
648
+ export {
649
+ AxiomTransport,
650
+ ConsoleTransport,
651
+ LogtapeTransport,
652
+ SentryTransport,
653
+ createAxiomTransport,
654
+ createLogtapeTransport,
655
+ createSentryTransport
656
+ };
657
+ //# sourceMappingURL=index.js.map