@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.
- package/dist/accessibility-auditor.js +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.d.ts +10 -0
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -63
- package/dist/index.js.map +2 -2
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +199 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
return { scrollCount, heightReached: previousHeight };
|
|
127
|
+
catch { /* non-fatal */ }
|
|
128
|
+
return { scrollCount, heightReached: previousHeight };
|
|
116
129
|
}
|
|
117
|
-
async function extractIframeContent(page) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
return results;
|
|
153
|
+
catch { /* non-fatal */ }
|
|
154
|
+
return results;
|
|
137
155
|
}
|
|
138
|
-
async function pierceShadowDom(page) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
return randomBytes(bytes).toString('hex');
|
|
6
18
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|