@detiq/crawler 1.1.7 → 1.1.9

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.
Files changed (92) hide show
  1. package/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.d.ts +10 -0
  22. package/dist/extractors/advanced.js +266 -249
  23. package/dist/extractors/artifact-envelope.js +58 -59
  24. package/dist/failure-analysis-agent.js +33 -37
  25. package/dist/feature-flag-detector.js +66 -51
  26. package/dist/form-detector.js +75 -55
  27. package/dist/frontier.js +101 -76
  28. package/dist/gap-pass.js +89 -68
  29. package/dist/graphql-detector.js +72 -62
  30. package/dist/http-engine.js +330 -282
  31. package/dist/incremental-diff.js +87 -54
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +28 -63
  34. package/dist/index.js.map +2 -2
  35. package/dist/input-fuzzer.js +97 -93
  36. package/dist/interactive-probe.d.ts +36 -0
  37. package/dist/interactive-probe.js +827 -715
  38. package/dist/job-queue.js +2169 -2151
  39. package/dist/js-coverage.js +56 -52
  40. package/dist/js-router-detector.js +117 -86
  41. package/dist/language-detector.js +63 -51
  42. package/dist/link-classifier.js +50 -32
  43. package/dist/live-view.js +108 -91
  44. package/dist/locator-extractor.d.ts +37 -0
  45. package/dist/locator-extractor.js +199 -0
  46. package/dist/locator-extractor.js.map +7 -0
  47. package/dist/logger.js +7 -9
  48. package/dist/markdown-extract.js +61 -48
  49. package/dist/native-crawler.js +946 -780
  50. package/dist/network-observer.js +156 -135
  51. package/dist/network-throttle.js +40 -29
  52. package/dist/openapi-generator.js +92 -77
  53. package/dist/pdf-extractor.js +90 -72
  54. package/dist/performance-metrics.js +102 -80
  55. package/dist/phash.js +144 -125
  56. package/dist/pii-scrubber.js +68 -65
  57. package/dist/playwright-recorder.js +58 -70
  58. package/dist/policies/crawl-safety.js +34 -31
  59. package/dist/profiles/domain-profile.js +125 -130
  60. package/dist/proxy.js +172 -146
  61. package/dist/rate-limiter.js +65 -58
  62. package/dist/redirect-tracker.js +64 -60
  63. package/dist/redis-frontier.js +136 -116
  64. package/dist/redis-queue.js +144 -157
  65. package/dist/response-cache.js +140 -127
  66. package/dist/responsive-detector.js +46 -42
  67. package/dist/retry-policy.js +50 -46
  68. package/dist/robots-txt.js +83 -69
  69. package/dist/screen-reader.js +70 -45
  70. package/dist/screenshot-masker.js +77 -59
  71. package/dist/seo-checker.js +107 -88
  72. package/dist/session-capture.js +212 -192
  73. package/dist/session-export.js +133 -107
  74. package/dist/sitemap-writer.js +41 -38
  75. package/dist/sitemap.js +202 -141
  76. package/dist/skyvern.js +148 -123
  77. package/dist/sqs-queue.js +42 -42
  78. package/dist/sso-token.js +90 -74
  79. package/dist/stealth/human-behavior.js +173 -156
  80. package/dist/telemetry.js +82 -68
  81. package/dist/test-executor.js +335 -309
  82. package/dist/tls-fingerprint.js +88 -77
  83. package/dist/url-pattern-completer.js +90 -71
  84. package/dist/visual-diff-verdict.js +38 -23
  85. package/dist/webhook-notifier.js +69 -71
  86. package/dist/websocket-detector.js +68 -56
  87. package/dist/worker.js +38 -29
  88. package/package.json +1 -1
  89. package/src/index.ts +2 -0
  90. package/src/locator-extractor.ts +226 -0
  91. package/types/index.d.ts +2 -0
  92. package/types/locator-extractor.d.ts +37 -0
@@ -1,169 +1,186 @@
1
+ // Gaussian random using Box-Muller transform
1
2
  function gaussian(mean, sigma) {
2
- const u1 = Math.random();
3
- const u2 = Math.random();
4
- const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
5
- return mean + z * sigma;
3
+ const u1 = Math.random();
4
+ const u2 = Math.random();
5
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
6
+ return mean + z * sigma;
6
7
  }
7
- async function humanDelay(min = 80, max = 450) {
8
- const mean = (min + max) / 2;
9
- const sigma = (max - min) / 6;
10
- const delay = Math.max(min, Math.min(max, gaussian(mean, sigma)));
11
- await new Promise((r) => setTimeout(r, Math.round(delay)));
8
+ export async function humanDelay(min = 80, max = 450) {
9
+ const mean = (min + max) / 2;
10
+ const sigma = (max - min) / 6;
11
+ const delay = Math.max(min, Math.min(max, gaussian(mean, sigma)));
12
+ await new Promise(r => setTimeout(r, Math.round(delay)));
12
13
  }
13
- async function simulateMouseMovement(page, targetX, targetY) {
14
- try {
15
- const mouse = page.mouse;
16
- const steps = 8 + Math.floor(Math.random() * 8);
17
- const startX = targetX - 100 + Math.random() * 200;
18
- const startY = targetY - 100 + Math.random() * 200;
19
- const cpX = (startX + targetX) / 2 + (Math.random() - 0.5) * 80;
20
- const cpY = (startY + targetY) / 2 + (Math.random() - 0.5) * 80;
21
- for (let i = 0; i <= steps; i++) {
22
- const t = i / steps;
23
- const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * cpX + t * t * targetX;
24
- const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * cpY + t * t * targetY;
25
- await mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2);
26
- if (i < steps) await humanDelay(8, 25);
14
+ export async function simulateMouseMovement(page, targetX, targetY) {
15
+ try {
16
+ const mouse = page.mouse;
17
+ const steps = 8 + Math.floor(Math.random() * 8);
18
+ // Generate Bezier control points with slight perpendicular offset
19
+ const startX = targetX - 100 + Math.random() * 200;
20
+ const startY = targetY - 100 + Math.random() * 200;
21
+ const cpX = (startX + targetX) / 2 + (Math.random() - 0.5) * 80;
22
+ const cpY = (startY + targetY) / 2 + (Math.random() - 0.5) * 80;
23
+ for (let i = 0; i <= steps; i++) {
24
+ const t = i / steps;
25
+ // Quadratic Bezier: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
26
+ const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * cpX + t * t * targetX;
27
+ const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * cpY + t * t * targetY;
28
+ await mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2);
29
+ if (i < steps)
30
+ await humanDelay(8, 25);
31
+ }
32
+ // Small overshoot correction
33
+ await mouse.move(targetX + (Math.random() - 0.5) * 4, targetY + (Math.random() - 0.5) * 4);
34
+ await humanDelay(30, 80);
35
+ await mouse.move(targetX, targetY);
27
36
  }
28
- await mouse.move(targetX + (Math.random() - 0.5) * 4, targetY + (Math.random() - 0.5) * 4);
29
- await humanDelay(30, 80);
30
- await mouse.move(targetX, targetY);
31
- } catch {
32
- }
37
+ catch { /* non-fatal */ }
33
38
  }
34
- async function simulateMouseHover(page, selector) {
35
- try {
36
- const el = await page.$(selector);
37
- if (!el) return;
38
- const box = await el.boundingBox();
39
- if (!box) return;
40
- const cx = box.x + box.width / 2;
41
- const cy = box.y + box.height / 2;
42
- await simulateMouseMovement(page, cx, cy);
43
- await humanDelay(100, 300);
44
- } catch {
45
- }
39
+ export async function simulateMouseHover(page, selector) {
40
+ try {
41
+ const el = await page.$(selector);
42
+ if (!el)
43
+ return;
44
+ const box = await el.boundingBox();
45
+ if (!box)
46
+ return;
47
+ const cx = box.x + box.width / 2;
48
+ const cy = box.y + box.height / 2;
49
+ await simulateMouseMovement(page, cx, cy);
50
+ await humanDelay(100, 300);
51
+ }
52
+ catch { /* non-fatal */ }
46
53
  }
47
- async function simulateClick(page, selector) {
48
- try {
49
- await simulateMouseHover(page, selector);
50
- await humanDelay(50, 150);
51
- await page.click(selector, { delay: Math.round(gaussian(80, 30)) });
52
- } catch {
53
- }
54
+ export async function simulateClick(page, selector) {
55
+ try {
56
+ await simulateMouseHover(page, selector);
57
+ await humanDelay(50, 150);
58
+ await page.click(selector, { delay: Math.round(gaussian(80, 30)) });
59
+ }
60
+ catch { /* non-fatal */ }
54
61
  }
55
- async function injectFingerprintOverrides(page) {
56
- await page.addInitScript(() => {
57
- const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
58
- HTMLCanvasElement.prototype.toDataURL = function(...args) {
59
- const ctx = this.getContext("2d");
60
- if (ctx) {
61
- const imageData = ctx.getImageData(0, 0, this.width, this.height);
62
- for (let i = 0; i < imageData.data.length; i += 4) {
63
- imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (Math.random() < 0.01 ? Math.random() > 0.5 ? 1 : -1 : 0)));
64
- }
65
- ctx.putImageData(imageData, 0, 0);
66
- }
67
- return origToDataURL.apply(this, args);
68
- };
69
- const origGetParam = WebGLRenderingContext.prototype.getParameter;
70
- WebGLRenderingContext.prototype.getParameter = function(parameter) {
71
- const UNMASKED_VENDOR_WEBGL = 37445;
72
- const UNMASKED_RENDERER_WEBGL = 37446;
73
- if (parameter === UNMASKED_VENDOR_WEBGL) return "Intel Inc.";
74
- if (parameter === UNMASKED_RENDERER_WEBGL) return "Intel Iris OpenGL Engine";
75
- return origGetParam.call(this, parameter);
76
- };
77
- const cpuCounts = [4, 6, 8, 8];
78
- Object.defineProperty(navigator, "hardwareConcurrency", {
79
- get: () => cpuCounts[Math.floor(Math.random() * cpuCounts.length)],
80
- configurable: true
81
- });
82
- Object.defineProperty(navigator, "deviceMemory", {
83
- get: () => [4, 8][Math.floor(Math.random() * 2)],
84
- configurable: true
85
- });
86
- }).catch(() => {
87
- });
62
+ export async function injectFingerprintOverrides(page) {
63
+ await page.addInitScript(() => {
64
+ // Canvas noise — subtle per-pixel perturbation
65
+ const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
66
+ HTMLCanvasElement.prototype.toDataURL = function (...args) {
67
+ const ctx = this.getContext('2d');
68
+ if (ctx) {
69
+ const imageData = ctx.getImageData(0, 0, this.width, this.height);
70
+ for (let i = 0; i < imageData.data.length; i += 4) {
71
+ imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (Math.random() < 0.01 ? (Math.random() > 0.5 ? 1 : -1) : 0)));
72
+ }
73
+ ctx.putImageData(imageData, 0, 0);
74
+ }
75
+ return origToDataURL.apply(this, args);
76
+ };
77
+ // WebGL parameter spoofing
78
+ const origGetParam = WebGLRenderingContext.prototype.getParameter;
79
+ WebGLRenderingContext.prototype.getParameter = function (parameter) {
80
+ const UNMASKED_VENDOR_WEBGL = 0x9245;
81
+ const UNMASKED_RENDERER_WEBGL = 0x9246;
82
+ if (parameter === UNMASKED_VENDOR_WEBGL)
83
+ return 'Intel Inc.';
84
+ if (parameter === UNMASKED_RENDERER_WEBGL)
85
+ return 'Intel Iris OpenGL Engine';
86
+ return origGetParam.call(this, parameter);
87
+ };
88
+ // Hardware concurrency — pick from common values
89
+ const cpuCounts = [4, 6, 8, 8];
90
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
91
+ get: () => cpuCounts[Math.floor(Math.random() * cpuCounts.length)],
92
+ configurable: true,
93
+ });
94
+ // Device memory
95
+ Object.defineProperty(navigator, 'deviceMemory', {
96
+ get: () => [4, 8][Math.floor(Math.random() * 2)],
97
+ configurable: true,
98
+ });
99
+ }).catch(() => { });
88
100
  }
89
- async function infiniteScrollToBottom(page, opts = {}) {
90
- const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;
91
- let scrollCount = 0;
92
- let stableScrollCount = 0;
93
- let previousHeight = 0;
94
- try {
95
- while (scrollCount < maxScrolls) {
96
- previousHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
97
- const scrollBy = 500 + Math.floor(Math.random() * 300);
98
- await page.evaluate((by) => window.scrollBy(0, by), scrollBy).catch(() => {
99
- });
100
- await humanDelay(scrollDelay - 100, scrollDelay + 200);
101
- await page.waitForTimeout(stabilizeMs).catch(() => {
102
- });
103
- scrollCount++;
104
- const newHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
105
- if (newHeight - previousHeight < 10) {
106
- stableScrollCount++;
107
- if (stableScrollCount >= 3) break;
108
- } else {
109
- stableScrollCount = 0;
110
- }
111
- previousHeight = newHeight;
101
+ export async function infiniteScrollToBottom(page, opts = {}) {
102
+ const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;
103
+ let scrollCount = 0;
104
+ let stableScrollCount = 0;
105
+ let previousHeight = 0;
106
+ try {
107
+ while (scrollCount < maxScrolls) {
108
+ previousHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
109
+ const scrollBy = 500 + Math.floor(Math.random() * 300);
110
+ await page.evaluate((by) => window.scrollBy(0, by), scrollBy).catch(() => { });
111
+ await humanDelay(scrollDelay - 100, scrollDelay + 200);
112
+ // Wait for potential lazy-loaded content
113
+ await page.waitForTimeout(stabilizeMs).catch(() => { });
114
+ scrollCount++;
115
+ const newHeight = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
116
+ if (newHeight - previousHeight < 10) {
117
+ stableScrollCount++;
118
+ if (stableScrollCount >= 3)
119
+ break; // no meaningful new content after 3 consecutive non-growing scrolls
120
+ }
121
+ else {
122
+ stableScrollCount = 0;
123
+ }
124
+ previousHeight = newHeight;
125
+ }
112
126
  }
113
- } catch {
114
- }
115
- return { scrollCount, heightReached: previousHeight };
127
+ catch { /* non-fatal */ }
128
+ return { scrollCount, heightReached: previousHeight };
116
129
  }
117
- async function extractIframeContent(page) {
118
- const results = [];
119
- try {
120
- const iframeSrcs = await page.evaluate(
121
- () => Array.from(document.querySelectorAll("iframe")).map((f) => f.getAttribute("src") ?? f.getAttribute("data-src") ?? "")
122
- ).catch(() => []);
123
- for (const frame of page.frames()) {
124
- const src = frame.url();
125
- if (!src || src === "about:blank" || src === page.url()) continue;
126
- try {
127
- const content = await frame.content().catch(() => "");
128
- if (!content || content.length < 50) continue;
129
- const text = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
130
- results.push({ src, content: content.slice(0, 1e4), markdown: text.slice(0, 5e3) });
131
- } catch {
132
- }
130
+ export async function extractIframeContent(page) {
131
+ const results = [];
132
+ try {
133
+ const iframeSrcs = await page.evaluate(() => Array.from(document.querySelectorAll('iframe')).map(f => f.getAttribute('src') ?? f.getAttribute('data-src') ?? '')).catch(() => []);
134
+ for (const frame of page.frames()) {
135
+ const src = frame.url();
136
+ if (!src || src === 'about:blank' || src === page.url())
137
+ continue;
138
+ try {
139
+ const content = await frame.content().catch(() => '');
140
+ if (!content || content.length < 50)
141
+ continue;
142
+ // Simple text extraction from iframe HTML
143
+ const text = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
144
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
145
+ .replace(/<[^>]+>/g, ' ')
146
+ .replace(/\s+/g, ' ')
147
+ .trim();
148
+ results.push({ src, content: content.slice(0, 10_000), markdown: text.slice(0, 5_000) });
149
+ }
150
+ catch { /* skip this frame */ }
151
+ }
133
152
  }
134
- } catch {
135
- }
136
- return results;
153
+ catch { /* non-fatal */ }
154
+ return results;
137
155
  }
138
- async function pierceShadowDom(page) {
139
- try {
140
- return await page.evaluate(() => {
141
- const parts = [];
142
- function walkShadow(root) {
143
- const shadow = root.shadowRoot;
144
- if (shadow) {
145
- parts.push(shadow.innerHTML ?? "");
146
- for (const child of [...shadow.querySelectorAll("*")]) walkShadow(child);
147
- }
148
- for (const child of Array.from(root.querySelectorAll(":not(script):not(style)"))) {
149
- if (child.shadowRoot) walkShadow(child);
150
- }
151
- }
152
- walkShadow(document.body);
153
- return parts.join("\n").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 1e4);
154
- }).catch(() => "");
155
- } catch {
156
- return "";
157
- }
156
+ export async function pierceShadowDom(page) {
157
+ try {
158
+ return await page.evaluate(() => {
159
+ const parts = [];
160
+ function walkShadow(root) {
161
+ const shadow = root.shadowRoot;
162
+ if (shadow) {
163
+ parts.push(shadow.innerHTML ?? '');
164
+ for (const child of [...shadow.querySelectorAll('*')])
165
+ walkShadow(child);
166
+ }
167
+ for (const child of Array.from(root.querySelectorAll(':not(script):not(style)'))) {
168
+ if (child.shadowRoot)
169
+ walkShadow(child);
170
+ }
171
+ }
172
+ walkShadow(document.body);
173
+ return parts
174
+ .join('\n')
175
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
176
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
177
+ .replace(/<[^>]+>/g, ' ')
178
+ .replace(/\s+/g, ' ')
179
+ .trim()
180
+ .slice(0, 10_000);
181
+ }).catch(() => '');
182
+ }
183
+ catch {
184
+ return '';
185
+ }
158
186
  }
159
- export {
160
- extractIframeContent,
161
- humanDelay,
162
- infiniteScrollToBottom,
163
- injectFingerprintOverrides,
164
- pierceShadowDom,
165
- simulateClick,
166
- simulateMouseHover,
167
- simulateMouseMovement
168
- };
169
- //# sourceMappingURL=human-behavior.js.map
package/dist/telemetry.js CHANGED
@@ -1,77 +1,91 @@
1
- import { randomBytes } from "node:crypto";
1
+ /**
2
+ * Lightweight OTel-compatible tracing for Zeta crawler.
3
+ *
4
+ * Emits spans as structured JSON logs compatible with OpenTelemetry format.
5
+ * Drop-in upgrade path: replace this module with @opentelemetry/sdk-node when needed.
6
+ *
7
+ * Usage:
8
+ * const span = startSpan('crawl.url', { url, engine });
9
+ * // ... do work ...
10
+ * endSpan(span, { screensFound: 3 });
11
+ */
12
+ import { randomBytes } from 'node:crypto';
13
+ // Active trace context — per async execution context (simplified: per-process global)
2
14
  let currentTraceId = null;
3
15
  let currentSpanId = null;
4
16
  function genId(bytes) {
5
- return randomBytes(bytes).toString("hex");
17
+ return randomBytes(bytes).toString('hex');
6
18
  }
7
- function startSpan(name, attributes = {}) {
8
- const traceId = currentTraceId ?? genId(16);
9
- const spanId = genId(8);
10
- const parentSpanId = currentSpanId ?? void 0;
11
- if (!currentTraceId) currentTraceId = traceId;
12
- currentSpanId = spanId;
13
- return { traceId, spanId, parentSpanId, name, startTimeMs: Date.now(), attributes };
19
+ /** Start a new span. If a trace is already active, the new span is a child. */
20
+ export function startSpan(name, attributes = {}) {
21
+ const traceId = currentTraceId ?? genId(16);
22
+ const spanId = genId(8);
23
+ const parentSpanId = currentSpanId ?? undefined;
24
+ if (!currentTraceId)
25
+ currentTraceId = traceId;
26
+ currentSpanId = spanId;
27
+ return { traceId, spanId, parentSpanId, name, startTimeMs: Date.now(), attributes };
14
28
  }
15
- function endSpan(span, extraAttributes = {}, error) {
16
- const endTimeMs = Date.now();
17
- const durationMs = endTimeMs - span.startTimeMs;
18
- const finished = {
19
- ...span,
20
- attributes: { ...span.attributes, ...extraAttributes },
21
- endTimeMs,
22
- durationMs,
23
- status: error ? "error" : "ok",
24
- error: error?.message
25
- };
26
- if (!span.parentSpanId) {
27
- currentTraceId = null;
28
- currentSpanId = span.parentSpanId ?? null;
29
- } else {
30
- currentSpanId = span.parentSpanId ?? null;
31
- }
32
- console.log(JSON.stringify({
33
- ts: new Date(endTimeMs).toISOString(),
34
- level: error ? "error" : "info",
35
- msg: `span:${finished.name}`,
36
- trace_id: finished.traceId,
37
- span_id: finished.spanId,
38
- parent_span_id: finished.parentSpanId,
39
- duration_ms: durationMs,
40
- status: finished.status,
41
- error: finished.error,
42
- ...finished.attributes
43
- }));
44
- return finished;
29
+ /** End a span and emit it as a structured log line. */
30
+ export function endSpan(span, extraAttributes = {}, error) {
31
+ const endTimeMs = Date.now();
32
+ const durationMs = endTimeMs - span.startTimeMs;
33
+ const finished = {
34
+ ...span,
35
+ attributes: { ...span.attributes, ...extraAttributes },
36
+ endTimeMs,
37
+ durationMs,
38
+ status: error ? 'error' : 'ok',
39
+ error: error?.message,
40
+ };
41
+ // Reset context when root span ends
42
+ if (!span.parentSpanId) {
43
+ currentTraceId = null;
44
+ currentSpanId = span.parentSpanId ?? null;
45
+ }
46
+ else {
47
+ currentSpanId = span.parentSpanId ?? null;
48
+ }
49
+ // Emit as structured JSON — compatible with Datadog/Grafana log parsing
50
+ console.log(JSON.stringify({
51
+ ts: new Date(endTimeMs).toISOString(),
52
+ level: error ? 'error' : 'info',
53
+ msg: `span:${finished.name}`,
54
+ trace_id: finished.traceId,
55
+ span_id: finished.spanId,
56
+ parent_span_id: finished.parentSpanId,
57
+ duration_ms: durationMs,
58
+ status: finished.status,
59
+ error: finished.error,
60
+ ...finished.attributes,
61
+ }));
62
+ return finished;
45
63
  }
46
- async function withSpan(name, attributes, fn) {
47
- const span = startSpan(name, attributes);
48
- try {
49
- const result = await fn(span);
50
- endSpan(span);
51
- return result;
52
- } catch (err) {
53
- endSpan(span, {}, err);
54
- throw err;
55
- }
64
+ /** Convenience: wrap an async function in a span. */
65
+ export async function withSpan(name, attributes, fn) {
66
+ const span = startSpan(name, attributes);
67
+ try {
68
+ const result = await fn(span);
69
+ endSpan(span);
70
+ return result;
71
+ }
72
+ catch (err) {
73
+ endSpan(span, {}, err);
74
+ throw err;
75
+ }
56
76
  }
57
- const counters = /* @__PURE__ */ new Map();
58
- function increment(metric, value = 1, tags = {}) {
59
- const key = metric + (Object.keys(tags).length ? ":" + Object.entries(tags).map(([k, v]) => `${k}=${v}`).join(",") : "");
60
- counters.set(key, (counters.get(key) ?? 0) + value);
77
+ /** Counter metrics — increment and log on flush. */
78
+ const counters = new Map();
79
+ export function increment(metric, value = 1, tags = {}) {
80
+ const key = metric + (Object.keys(tags).length ? ':' + Object.entries(tags).map(([k, v]) => `${k}=${v}`).join(',') : '');
81
+ counters.set(key, (counters.get(key) ?? 0) + value);
61
82
  }
62
- function flushMetrics() {
63
- const snapshot = Object.fromEntries(counters);
64
- if (Object.keys(snapshot).length > 0) {
65
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "metrics:flush", ...snapshot }));
66
- counters.clear();
67
- }
68
- return snapshot;
83
+ /** Flush counters as a structured log — call periodically (e.g. at job end). */
84
+ export function flushMetrics() {
85
+ const snapshot = Object.fromEntries(counters);
86
+ if (Object.keys(snapshot).length > 0) {
87
+ console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'info', msg: 'metrics:flush', ...snapshot }));
88
+ counters.clear();
89
+ }
90
+ return snapshot;
69
91
  }
70
- export {
71
- endSpan,
72
- flushMetrics,
73
- increment,
74
- startSpan,
75
- withSpan
76
- };
77
- //# sourceMappingURL=telemetry.js.map