@detiq/crawler 0.1.3

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 (412) hide show
  1. package/Dockerfile +49 -0
  2. package/README.md +11 -0
  3. package/dist/accessibility-auditor.d.ts +29 -0
  4. package/dist/accessibility-auditor.js +57 -0
  5. package/dist/accessibility-auditor.js.map +7 -0
  6. package/dist/ai-login.d.ts +32 -0
  7. package/dist/ai-login.js +177 -0
  8. package/dist/ai-login.js.map +7 -0
  9. package/dist/auth-token-rotator.d.ts +47 -0
  10. package/dist/auth-token-rotator.js +58 -0
  11. package/dist/auth-token-rotator.js.map +7 -0
  12. package/dist/auth.d.ts +20 -0
  13. package/dist/auth.js +329 -0
  14. package/dist/auth.js.map +7 -0
  15. package/dist/browser-extension.d.ts +37 -0
  16. package/dist/browser-extension.js +113 -0
  17. package/dist/browser-extension.js.map +7 -0
  18. package/dist/captcha-solver.d.ts +16 -0
  19. package/dist/captcha-solver.js +105 -0
  20. package/dist/captcha-solver.js.map +7 -0
  21. package/dist/color-contrast.d.ts +30 -0
  22. package/dist/color-contrast.js +78 -0
  23. package/dist/color-contrast.js.map +7 -0
  24. package/dist/content-chunker.d.ts +18 -0
  25. package/dist/content-chunker.js +152 -0
  26. package/dist/content-chunker.js.map +7 -0
  27. package/dist/content-dedup.d.ts +23 -0
  28. package/dist/content-dedup.js +60 -0
  29. package/dist/content-dedup.js.map +7 -0
  30. package/dist/cookie-consent.d.ts +25 -0
  31. package/dist/cookie-consent.js +78 -0
  32. package/dist/cookie-consent.js.map +7 -0
  33. package/dist/cookie-jar.d.ts +20 -0
  34. package/dist/cookie-jar.js +61 -0
  35. package/dist/cookie-jar.js.map +7 -0
  36. package/dist/crawler.d.ts +179 -0
  37. package/dist/crawler.js +3672 -0
  38. package/dist/crawler.js.map +7 -0
  39. package/dist/custom-js-injector.d.ts +42 -0
  40. package/dist/custom-js-injector.js +50 -0
  41. package/dist/custom-js-injector.js.map +7 -0
  42. package/dist/dark-mode-capture.d.ts +23 -0
  43. package/dist/dark-mode-capture.js +75 -0
  44. package/dist/dark-mode-capture.js.map +7 -0
  45. package/dist/dead-link-detector.d.ts +37 -0
  46. package/dist/dead-link-detector.js +50 -0
  47. package/dist/dead-link-detector.js.map +7 -0
  48. package/dist/element-hash.d.ts +9 -0
  49. package/dist/element-hash.js +9 -0
  50. package/dist/element-hash.js.map +7 -0
  51. package/dist/email-otp.d.ts +42 -0
  52. package/dist/email-otp.js +177 -0
  53. package/dist/email-otp.js.map +7 -0
  54. package/dist/engine-router.d.ts +47 -0
  55. package/dist/engine-router.js +94 -0
  56. package/dist/engine-router.js.map +7 -0
  57. package/dist/export/bulk-export.d.ts +26 -0
  58. package/dist/export/bulk-export.js +76 -0
  59. package/dist/export/bulk-export.js.map +7 -0
  60. package/dist/export/vector-export.d.ts +76 -0
  61. package/dist/export/vector-export.js +85 -0
  62. package/dist/export/vector-export.js.map +7 -0
  63. package/dist/extractors/advanced.d.ts +54 -0
  64. package/dist/extractors/advanced.js +245 -0
  65. package/dist/extractors/advanced.js.map +7 -0
  66. package/dist/extractors/artifact-envelope.d.ts +63 -0
  67. package/dist/extractors/artifact-envelope.js +63 -0
  68. package/dist/extractors/artifact-envelope.js.map +7 -0
  69. package/dist/failure-analysis-agent.d.ts +16 -0
  70. package/dist/failure-analysis-agent.js +40 -0
  71. package/dist/failure-analysis-agent.js.map +7 -0
  72. package/dist/feature-flag-detector.d.ts +30 -0
  73. package/dist/feature-flag-detector.js +55 -0
  74. package/dist/feature-flag-detector.js.map +7 -0
  75. package/dist/form-detector.d.ts +34 -0
  76. package/dist/form-detector.js +58 -0
  77. package/dist/form-detector.js.map +7 -0
  78. package/dist/frontier.d.ts +32 -0
  79. package/dist/frontier.js +83 -0
  80. package/dist/frontier.js.map +7 -0
  81. package/dist/gap-pass.d.ts +35 -0
  82. package/dist/gap-pass.js +81 -0
  83. package/dist/gap-pass.js.map +7 -0
  84. package/dist/graphql-detector.d.ts +39 -0
  85. package/dist/graphql-detector.js +86 -0
  86. package/dist/graphql-detector.js.map +7 -0
  87. package/dist/http-engine.d.ts +80 -0
  88. package/dist/http-engine.js +304 -0
  89. package/dist/http-engine.js.map +7 -0
  90. package/dist/incremental-diff.d.ts +59 -0
  91. package/dist/incremental-diff.js +61 -0
  92. package/dist/incremental-diff.js.map +7 -0
  93. package/dist/index.d.ts +35 -0
  94. package/dist/index.js +61 -0
  95. package/dist/index.js.map +7 -0
  96. package/dist/input-fuzzer.d.ts +30 -0
  97. package/dist/input-fuzzer.js +105 -0
  98. package/dist/input-fuzzer.js.map +7 -0
  99. package/dist/interactive-probe.d.ts +104 -0
  100. package/dist/interactive-probe.js +442 -0
  101. package/dist/interactive-probe.js.map +7 -0
  102. package/dist/job-queue.d.ts +171 -0
  103. package/dist/job-queue.js +2256 -0
  104. package/dist/job-queue.js.map +7 -0
  105. package/dist/js-coverage.d.ts +29 -0
  106. package/dist/js-coverage.js +56 -0
  107. package/dist/js-coverage.js.map +7 -0
  108. package/dist/js-router-detector.d.ts +17 -0
  109. package/dist/js-router-detector.js +90 -0
  110. package/dist/js-router-detector.js.map +7 -0
  111. package/dist/language-detector.d.ts +22 -0
  112. package/dist/language-detector.js +57 -0
  113. package/dist/language-detector.js.map +7 -0
  114. package/dist/link-classifier.d.ts +23 -0
  115. package/dist/link-classifier.js +40 -0
  116. package/dist/link-classifier.js.map +7 -0
  117. package/dist/live-view.d.ts +36 -0
  118. package/dist/live-view.js +100 -0
  119. package/dist/live-view.js.map +7 -0
  120. package/dist/logger.d.ts +2 -0
  121. package/dist/logger.js +10 -0
  122. package/dist/logger.js.map +7 -0
  123. package/dist/markdown-extract.d.ts +8 -0
  124. package/dist/markdown-extract.js +54 -0
  125. package/dist/markdown-extract.js.map +7 -0
  126. package/dist/native-crawler.d.ts +69 -0
  127. package/dist/native-crawler.js +839 -0
  128. package/dist/native-crawler.js.map +7 -0
  129. package/dist/network-observer.d.ts +49 -0
  130. package/dist/network-observer.js +149 -0
  131. package/dist/network-observer.js.map +7 -0
  132. package/dist/network-throttle.d.ts +25 -0
  133. package/dist/network-throttle.js +34 -0
  134. package/dist/network-throttle.js.map +7 -0
  135. package/dist/openapi-generator.d.ts +49 -0
  136. package/dist/openapi-generator.js +89 -0
  137. package/dist/openapi-generator.js.map +7 -0
  138. package/dist/pdf-extractor.d.ts +28 -0
  139. package/dist/pdf-extractor.js +81 -0
  140. package/dist/pdf-extractor.js.map +7 -0
  141. package/dist/performance-metrics.d.ts +53 -0
  142. package/dist/performance-metrics.js +83 -0
  143. package/dist/performance-metrics.js.map +7 -0
  144. package/dist/phash.d.ts +38 -0
  145. package/dist/phash.js +115 -0
  146. package/dist/phash.js.map +7 -0
  147. package/dist/pii-scrubber.d.ts +21 -0
  148. package/dist/pii-scrubber.js +75 -0
  149. package/dist/pii-scrubber.js.map +7 -0
  150. package/dist/playwright-recorder.d.ts +39 -0
  151. package/dist/playwright-recorder.js +123 -0
  152. package/dist/playwright-recorder.js.map +7 -0
  153. package/dist/policies/crawl-safety.d.ts +9 -0
  154. package/dist/policies/crawl-safety.js +37 -0
  155. package/dist/policies/crawl-safety.js.map +7 -0
  156. package/dist/profiles/domain-profile.d.ts +19 -0
  157. package/dist/profiles/domain-profile.js +140 -0
  158. package/dist/profiles/domain-profile.js.map +7 -0
  159. package/dist/proxy.d.ts +50 -0
  160. package/dist/proxy.js +165 -0
  161. package/dist/proxy.js.map +7 -0
  162. package/dist/rate-limiter.d.ts +31 -0
  163. package/dist/rate-limiter.js +64 -0
  164. package/dist/rate-limiter.js.map +7 -0
  165. package/dist/redirect-tracker.d.ts +37 -0
  166. package/dist/redirect-tracker.js +65 -0
  167. package/dist/redirect-tracker.js.map +7 -0
  168. package/dist/redis-frontier.d.ts +48 -0
  169. package/dist/redis-frontier.js +126 -0
  170. package/dist/redis-frontier.js.map +7 -0
  171. package/dist/redis-queue.d.ts +48 -0
  172. package/dist/redis-queue.js +165 -0
  173. package/dist/redis-queue.js.map +7 -0
  174. package/dist/response-cache.d.ts +38 -0
  175. package/dist/response-cache.js +140 -0
  176. package/dist/response-cache.js.map +7 -0
  177. package/dist/responsive-detector.d.ts +29 -0
  178. package/dist/responsive-detector.js +46 -0
  179. package/dist/responsive-detector.js.map +7 -0
  180. package/dist/retry-policy.d.ts +20 -0
  181. package/dist/retry-policy.js +54 -0
  182. package/dist/retry-policy.js.map +7 -0
  183. package/dist/robots-txt.d.ts +9 -0
  184. package/dist/robots-txt.js +77 -0
  185. package/dist/robots-txt.js.map +7 -0
  186. package/dist/screen-reader.d.ts +16 -0
  187. package/dist/screen-reader.js +46 -0
  188. package/dist/screen-reader.js.map +7 -0
  189. package/dist/screenshot-masker.d.ts +28 -0
  190. package/dist/screenshot-masker.js +62 -0
  191. package/dist/screenshot-masker.js.map +7 -0
  192. package/dist/seo-checker.d.ts +33 -0
  193. package/dist/seo-checker.js +91 -0
  194. package/dist/seo-checker.js.map +7 -0
  195. package/dist/session-capture.d.ts +35 -0
  196. package/dist/session-capture.js +267 -0
  197. package/dist/session-capture.js.map +7 -0
  198. package/dist/session-export.d.ts +49 -0
  199. package/dist/session-export.js +117 -0
  200. package/dist/session-export.js.map +7 -0
  201. package/dist/sitemap-writer.d.ts +21 -0
  202. package/dist/sitemap-writer.js +47 -0
  203. package/dist/sitemap-writer.js.map +7 -0
  204. package/dist/sitemap.d.ts +51 -0
  205. package/dist/sitemap.js +152 -0
  206. package/dist/sitemap.js.map +7 -0
  207. package/dist/skyvern.d.ts +56 -0
  208. package/dist/skyvern.js +129 -0
  209. package/dist/skyvern.js.map +7 -0
  210. package/dist/sqs-queue.d.ts +2 -0
  211. package/dist/sqs-queue.js +47 -0
  212. package/dist/sqs-queue.js.map +7 -0
  213. package/dist/sso-token.d.ts +29 -0
  214. package/dist/sso-token.js +76 -0
  215. package/dist/sso-token.js.map +7 -0
  216. package/dist/stealth/human-behavior.d.ts +20 -0
  217. package/dist/stealth/human-behavior.js +168 -0
  218. package/dist/stealth/human-behavior.js.map +7 -0
  219. package/dist/telemetry.d.ts +34 -0
  220. package/dist/telemetry.js +77 -0
  221. package/dist/telemetry.js.map +7 -0
  222. package/dist/test-executor.d.ts +67 -0
  223. package/dist/test-executor.js +311 -0
  224. package/dist/test-executor.js.map +7 -0
  225. package/dist/tls-fingerprint.d.ts +50 -0
  226. package/dist/tls-fingerprint.js +81 -0
  227. package/dist/tls-fingerprint.js.map +7 -0
  228. package/dist/url-pattern-completer.d.ts +28 -0
  229. package/dist/url-pattern-completer.js +87 -0
  230. package/dist/url-pattern-completer.js.map +7 -0
  231. package/dist/visual-diff-verdict.d.ts +40 -0
  232. package/dist/visual-diff-verdict.js +25 -0
  233. package/dist/visual-diff-verdict.js.map +7 -0
  234. package/dist/webhook-notifier.d.ts +30 -0
  235. package/dist/webhook-notifier.js +76 -0
  236. package/dist/webhook-notifier.js.map +7 -0
  237. package/dist/websocket-detector.d.ts +24 -0
  238. package/dist/websocket-detector.js +60 -0
  239. package/dist/websocket-detector.js.map +7 -0
  240. package/dist/worker.d.ts +1 -0
  241. package/dist/worker.js +33 -0
  242. package/dist/worker.js.map +7 -0
  243. package/package.json +45 -0
  244. package/src/accessibility-auditor.ts +102 -0
  245. package/src/ai-login.ts +218 -0
  246. package/src/auth-token-rotator.ts +89 -0
  247. package/src/auth.ts +396 -0
  248. package/src/browser-extension.ts +186 -0
  249. package/src/captcha-solver.ts +143 -0
  250. package/src/color-contrast.ts +116 -0
  251. package/src/content-chunker.ts +170 -0
  252. package/src/content-dedup.ts +99 -0
  253. package/src/cookie-consent.ts +106 -0
  254. package/src/cookie-jar.ts +91 -0
  255. package/src/crawler.ts +4305 -0
  256. package/src/custom-js-injector.ts +93 -0
  257. package/src/dark-mode-capture.ts +110 -0
  258. package/src/dead-link-detector.ts +76 -0
  259. package/src/element-hash.ts +16 -0
  260. package/src/email-otp.ts +252 -0
  261. package/src/engine-router.ts +141 -0
  262. package/src/export/bulk-export.ts +85 -0
  263. package/src/export/vector-export.ts +160 -0
  264. package/src/extractors/advanced.ts +283 -0
  265. package/src/extractors/artifact-envelope.ts +111 -0
  266. package/src/failure-analysis-agent.ts +49 -0
  267. package/src/feature-flag-detector.ts +97 -0
  268. package/src/form-detector.ts +103 -0
  269. package/src/frontier.test.ts +64 -0
  270. package/src/frontier.ts +112 -0
  271. package/src/gap-pass.ts +122 -0
  272. package/src/graphql-detector.ts +129 -0
  273. package/src/http-engine.ts +433 -0
  274. package/src/incremental-diff.ts +121 -0
  275. package/src/index.ts +35 -0
  276. package/src/input-fuzzer.ts +129 -0
  277. package/src/interactive-probe.ts +623 -0
  278. package/src/job-queue.test.ts +120 -0
  279. package/src/job-queue.ts +2701 -0
  280. package/src/js-coverage.ts +87 -0
  281. package/src/js-router-detector.ts +112 -0
  282. package/src/language-detector.ts +82 -0
  283. package/src/link-classifier.ts +65 -0
  284. package/src/live-view.ts +141 -0
  285. package/src/logger.ts +9 -0
  286. package/src/markdown-extract.test.ts +73 -0
  287. package/src/markdown-extract.ts +76 -0
  288. package/src/native-crawler.ts +1106 -0
  289. package/src/network-observer.ts +204 -0
  290. package/src/network-throttle.ts +69 -0
  291. package/src/openapi-generator.ts +135 -0
  292. package/src/pdf-extractor.ts +123 -0
  293. package/src/performance-metrics.ts +134 -0
  294. package/src/phash.ts +151 -0
  295. package/src/pii-scrubber.ts +99 -0
  296. package/src/playwright-recorder.ts +170 -0
  297. package/src/policies/crawl-safety.ts +46 -0
  298. package/src/profiles/domain-profile.ts +167 -0
  299. package/src/proxy.ts +238 -0
  300. package/src/rate-limiter.ts +87 -0
  301. package/src/redirect-tracker.ts +97 -0
  302. package/src/redis-frontier.ts +143 -0
  303. package/src/redis-queue.ts +208 -0
  304. package/src/response-cache.ts +150 -0
  305. package/src/responsive-detector.ts +92 -0
  306. package/src/retry-policy.ts +87 -0
  307. package/src/robots-txt.ts +96 -0
  308. package/src/screen-reader.ts +89 -0
  309. package/src/screenshot-masker.ts +98 -0
  310. package/src/seo-checker.ts +134 -0
  311. package/src/session-capture.ts +325 -0
  312. package/src/session-export.test.ts +75 -0
  313. package/src/session-export.ts +172 -0
  314. package/src/sitemap-writer.ts +65 -0
  315. package/src/sitemap.test.ts +131 -0
  316. package/src/sitemap.ts +224 -0
  317. package/src/skyvern.ts +199 -0
  318. package/src/sqs-queue.ts +49 -0
  319. package/src/sso-token.ts +120 -0
  320. package/src/stealth/human-behavior.ts +191 -0
  321. package/src/telemetry.ts +122 -0
  322. package/src/test-executor.test.ts +72 -0
  323. package/src/test-executor.ts +424 -0
  324. package/src/tls-fingerprint.ts +111 -0
  325. package/src/url-pattern-completer.ts +114 -0
  326. package/src/visual-diff-verdict.test.ts +80 -0
  327. package/src/visual-diff-verdict.ts +57 -0
  328. package/src/webhook-notifier.ts +109 -0
  329. package/src/websocket-detector.ts +83 -0
  330. package/src/worker.ts +48 -0
  331. package/tsconfig.json +13 -0
  332. package/types/accessibility-auditor.d.ts +29 -0
  333. package/types/ai-login.d.ts +32 -0
  334. package/types/auth-token-rotator.d.ts +47 -0
  335. package/types/auth.d.ts +20 -0
  336. package/types/browser-extension.d.ts +37 -0
  337. package/types/bull-worker.d.ts +6 -0
  338. package/types/captcha-solver.d.ts +16 -0
  339. package/types/color-contrast.d.ts +30 -0
  340. package/types/content-chunker.d.ts +18 -0
  341. package/types/content-dedup.d.ts +23 -0
  342. package/types/cookie-consent.d.ts +25 -0
  343. package/types/cookie-jar.d.ts +20 -0
  344. package/types/crawler.d.ts +179 -0
  345. package/types/custom-js-injector.d.ts +42 -0
  346. package/types/dark-mode-capture.d.ts +23 -0
  347. package/types/dead-link-detector.d.ts +37 -0
  348. package/types/element-hash.d.ts +9 -0
  349. package/types/email-otp.d.ts +42 -0
  350. package/types/engine-router.d.ts +47 -0
  351. package/types/export/bulk-export.d.ts +26 -0
  352. package/types/export/vector-export.d.ts +76 -0
  353. package/types/extractors/advanced.d.ts +54 -0
  354. package/types/extractors/artifact-envelope.d.ts +63 -0
  355. package/types/failure-analysis-agent.d.ts +16 -0
  356. package/types/feature-flag-detector.d.ts +30 -0
  357. package/types/form-detector.d.ts +34 -0
  358. package/types/frontier.d.ts +32 -0
  359. package/types/gap-pass.d.ts +35 -0
  360. package/types/graphql-detector.d.ts +39 -0
  361. package/types/http-engine.d.ts +80 -0
  362. package/types/incremental-diff.d.ts +59 -0
  363. package/types/index.d.ts +35 -0
  364. package/types/input-fuzzer.d.ts +30 -0
  365. package/types/interactive-probe.d.ts +104 -0
  366. package/types/job-queue.d.ts +171 -0
  367. package/types/js-coverage.d.ts +29 -0
  368. package/types/js-router-detector.d.ts +17 -0
  369. package/types/language-detector.d.ts +22 -0
  370. package/types/link-classifier.d.ts +23 -0
  371. package/types/live-view.d.ts +36 -0
  372. package/types/logger.d.ts +2 -0
  373. package/types/markdown-extract.d.ts +8 -0
  374. package/types/native-crawler.d.ts +69 -0
  375. package/types/network-observer.d.ts +49 -0
  376. package/types/network-throttle.d.ts +25 -0
  377. package/types/openapi-generator.d.ts +49 -0
  378. package/types/pdf-extractor.d.ts +28 -0
  379. package/types/performance-metrics.d.ts +53 -0
  380. package/types/phash.d.ts +38 -0
  381. package/types/pii-scrubber.d.ts +21 -0
  382. package/types/playwright-recorder.d.ts +39 -0
  383. package/types/policies/crawl-safety.d.ts +9 -0
  384. package/types/profiles/domain-profile.d.ts +19 -0
  385. package/types/proxy.d.ts +50 -0
  386. package/types/rate-limiter.d.ts +31 -0
  387. package/types/redirect-tracker.d.ts +37 -0
  388. package/types/redis-frontier.d.ts +48 -0
  389. package/types/redis-queue.d.ts +48 -0
  390. package/types/response-cache.d.ts +38 -0
  391. package/types/responsive-detector.d.ts +29 -0
  392. package/types/retry-policy.d.ts +20 -0
  393. package/types/robots-txt.d.ts +9 -0
  394. package/types/screen-reader.d.ts +16 -0
  395. package/types/screenshot-masker.d.ts +28 -0
  396. package/types/seo-checker.d.ts +33 -0
  397. package/types/session-capture.d.ts +38 -0
  398. package/types/session-export.d.ts +49 -0
  399. package/types/sitemap-writer.d.ts +21 -0
  400. package/types/sitemap.d.ts +51 -0
  401. package/types/skyvern.d.ts +56 -0
  402. package/types/sqs-queue.d.ts +2 -0
  403. package/types/sso-token.d.ts +29 -0
  404. package/types/stealth/human-behavior.d.ts +20 -0
  405. package/types/telemetry.d.ts +34 -0
  406. package/types/test-executor.d.ts +67 -0
  407. package/types/tls-fingerprint.d.ts +50 -0
  408. package/types/url-pattern-completer.d.ts +28 -0
  409. package/types/visual-diff-verdict.d.ts +40 -0
  410. package/types/webhook-notifier.d.ts +30 -0
  411. package/types/websocket-detector.d.ts +24 -0
  412. package/types/worker.d.ts +1 -0
package/src/skyvern.ts ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Skyvern Vision Fallback
3
+ *
4
+ * Skyvern combines computer vision + LLM reasoning to navigate sites that
5
+ * defeat DOM-based approaches: CAPTCHAs, legacy apps, visual-only login flows,
6
+ * government portals. It's the "break glass" option when Stagehand fails.
7
+ *
8
+ * Production setup (Phase 4+):
9
+ * 1. Self-host: https://github.com/Skyvern-AI/skyvern
10
+ * 2. Or use cloud: https://app.skyvern.com
11
+ * 3. Set SKYVERN_API_URL + SKYVERN_API_KEY in .env
12
+ *
13
+ * Pricing note: vision tasks cost more per step than DOM-based Stagehand.
14
+ * Use only when the fast path fails.
15
+ */
16
+
17
+ export interface SkyvernTask {
18
+ taskDescription: string;
19
+ url: string;
20
+ credentials?: { username: string; password: string; oneTimeCode?: string };
21
+ }
22
+
23
+ export interface SkyvernConfig {
24
+ apiUrl: string;
25
+ apiKey: string;
26
+ llmKey?: string;
27
+ llmApiKey?: string;
28
+ }
29
+
30
+ export interface SkyvernStep {
31
+ order: number;
32
+ action_type: string;
33
+ element_description?: string;
34
+ reasoning?: string;
35
+ success: boolean;
36
+ screenshot_url?: string;
37
+ duration_ms?: number;
38
+ error?: string;
39
+ }
40
+
41
+ export interface SkyvernNavigateResult {
42
+ screenshotUrls: string[];
43
+ visitedUrls: string[];
44
+ status: 'success' | 'failed' | 'partial';
45
+ steps?: SkyvernStep[];
46
+ taskId?: string;
47
+ durationMs?: number;
48
+ triggerReason?: string;
49
+ }
50
+
51
+ /**
52
+ * Gap 5: Post-login navigation via Skyvern vision for apps where DOM analysis
53
+ * cannot discover screens (canvas, WebGL, legacy Flash-replacement stacks).
54
+ * Skyvern drives the browser via screenshot + LLM, returning visited page list.
55
+ */
56
+ export async function skyvernNavigate(
57
+ startUrl: string,
58
+ navigationGoals: string[],
59
+ tenantConfig?: SkyvernConfig,
60
+ triggerReason?: string,
61
+ ): Promise<SkyvernNavigateResult> {
62
+ const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;
63
+ const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;
64
+
65
+ if (!apiUrl || !apiKey) {
66
+ console.warn('[skyvern] skyvernNavigate: no API config — skipping post-login vision navigation');
67
+ return { screenshotUrls: [], visitedUrls: [], status: 'failed' };
68
+ }
69
+
70
+ const visitedUrls: string[] = [];
71
+ const screenshotUrls: string[] = [];
72
+ const skyvernSteps: SkyvernStep[] = [];
73
+ let lastTaskId: string | undefined;
74
+ let totalDurationMs = 0;
75
+
76
+ for (const goal of navigationGoals) {
77
+ try {
78
+ const taskStart = Date.now();
79
+ const res = await fetch(`${apiUrl}/api/v1/tasks`, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
82
+ body: JSON.stringify({
83
+ url: startUrl,
84
+ navigation_goal: goal,
85
+ ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),
86
+ ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),
87
+ }),
88
+ });
89
+ if (!res.ok) continue;
90
+ const data: any = await res.json();
91
+ const taskId: string = data.task_id;
92
+ if (!taskId) continue;
93
+ lastTaskId = taskId;
94
+
95
+ // Poll for task completion (max 60s per goal)
96
+ let done = false;
97
+ for (let p = 0; p < 12 && !done; p++) {
98
+ await new Promise((r) => setTimeout(r, 5000));
99
+ const poll = await fetch(`${apiUrl}/api/v1/tasks/${taskId}`, {
100
+ headers: { 'x-api-key': apiKey },
101
+ }).catch(() => null);
102
+ if (!poll?.ok) continue;
103
+ const pollData: any = await poll.json();
104
+ if (pollData.status === 'completed' || pollData.status === 'failed') {
105
+ done = true;
106
+ totalDurationMs += Date.now() - taskStart;
107
+ if (pollData.status === 'completed') {
108
+ if (pollData.output?.url) visitedUrls.push(pollData.output.url);
109
+ if (pollData.output?.screenshot_url) screenshotUrls.push(pollData.output.screenshot_url);
110
+ }
111
+
112
+ // Fetch step-level detail from Skyvern
113
+ try {
114
+ const stepsRes = await fetch(`${apiUrl}/api/v1/tasks/${taskId}/steps`, {
115
+ headers: { 'x-api-key': apiKey },
116
+ }).catch(() => null);
117
+ if (stepsRes?.ok) {
118
+ const stepsData: any = await stepsRes.json();
119
+ const stepsList = Array.isArray(stepsData) ? stepsData : (stepsData?.steps ?? []);
120
+ for (const s of stepsList) {
121
+ if (s.output?.screenshot_url) screenshotUrls.push(s.output.screenshot_url);
122
+ skyvernSteps.push({
123
+ order: s.step_order ?? s.order ?? 0,
124
+ action_type: s.action_type ?? s.action ?? 'unknown',
125
+ element_description: s.output?.element_description,
126
+ reasoning: s.output?.llm_response?.action_description ?? s.output?.reasoning,
127
+ success: s.status === 'completed',
128
+ screenshot_url: s.output?.screenshot_url,
129
+ error: s.status === 'failed' ? (s.output?.errors?.[0] ?? 'Step failed') : undefined,
130
+ });
131
+ }
132
+ }
133
+ } catch { /* non-fatal */ }
134
+ }
135
+ }
136
+ } catch (err) {
137
+ console.warn(`[skyvern] Navigation goal failed: ${goal}`, err);
138
+ }
139
+ }
140
+
141
+ return {
142
+ screenshotUrls,
143
+ visitedUrls,
144
+ status: visitedUrls.length > 0 ? 'success' : screenshotUrls.length > 0 ? 'partial' : 'failed',
145
+ steps: skyvernSteps,
146
+ taskId: lastTaskId,
147
+ durationMs: totalDurationMs || undefined,
148
+ triggerReason,
149
+ };
150
+ }
151
+
152
+ export async function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernConfig): Promise<'success' | 'failed'> {
153
+ const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;
154
+ const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;
155
+
156
+ if (!apiUrl || !apiKey) {
157
+ console.warn(
158
+ '[skyvern] Vision fallback disabled — set SKYVERN_API_URL + SKYVERN_API_KEY in .env to enable.',
159
+ '\n[skyvern] Self-host: https://github.com/Skyvern-AI/skyvern'
160
+ );
161
+ return 'failed';
162
+ }
163
+
164
+ try {
165
+ const res = await fetch(`${apiUrl}/api/v1/tasks`, {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
168
+ body: JSON.stringify({
169
+ url: task.url,
170
+ navigation_goal: task.taskDescription,
171
+ ...(task.credentials && {
172
+ navigation_payload: {
173
+ username: task.credentials.username,
174
+ password: task.credentials.password,
175
+ ...(task.credentials.oneTimeCode && { otp: task.credentials.oneTimeCode }),
176
+ },
177
+ }),
178
+ ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),
179
+ ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),
180
+ }),
181
+ });
182
+
183
+ if (!res.ok) {
184
+ const body = await res.text().catch(() => '');
185
+ console.error(`[skyvern] Task creation failed: ${res.status}`, body);
186
+ return 'failed';
187
+ }
188
+
189
+ const data: any = await res.json();
190
+ console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);
191
+ // Full implementation: poll until status === "completed" | "failed".
192
+ // For Phase 1 the login already happened in the browser context Skyvern manages,
193
+ // so returning 'success' here means we can proceed in that browser session.
194
+ return 'success';
195
+ } catch (err) {
196
+ console.error('[skyvern] Error:', err);
197
+ return 'failed';
198
+ }
199
+ }
@@ -0,0 +1,49 @@
1
+ import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
2
+
3
+ const sqs = new SQSClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
4
+
5
+ export async function sendJobToSqs(jobId: string, tenantId: string, config: any): Promise<void> {
6
+ const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
7
+ if (!queueUrl) throw new Error('SQS_CRAWL_QUEUE_URL not configured');
8
+ await sqs.send(new SendMessageCommand({
9
+ QueueUrl: queueUrl,
10
+ MessageBody: JSON.stringify({ jobId, tenantId, config }),
11
+ }));
12
+ }
13
+
14
+ export async function startSqsWorker(): Promise<void> {
15
+ const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
16
+ if (!queueUrl) throw new Error('SQS_CRAWL_QUEUE_URL not configured');
17
+
18
+ const { processJob } = await import('./job-queue.js');
19
+ console.log('[sqs-worker] starting, queue:', queueUrl);
20
+
21
+ while (true) {
22
+ try {
23
+ const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
24
+ QueueUrl: queueUrl,
25
+ WaitTimeSeconds: 20,
26
+ MaxNumberOfMessages: 2,
27
+ }));
28
+
29
+ await Promise.all(Messages.map(async (msg) => {
30
+ let jobId = '?';
31
+ try {
32
+ const data = JSON.parse(msg.Body!);
33
+ jobId = data.jobId;
34
+ await processJob(data.jobId, data.config);
35
+ await sqs.send(new DeleteMessageCommand({
36
+ QueueUrl: queueUrl,
37
+ ReceiptHandle: msg.ReceiptHandle!,
38
+ }));
39
+ } catch (err: any) {
40
+ console.error(`[sqs-worker] job ${jobId} failed — leaving for SQS redrive:`, err.message);
41
+ // Do NOT delete — SQS redrive policy + DLQ handles retry
42
+ }
43
+ }));
44
+ } catch (err: any) {
45
+ console.error('[sqs-worker] receive error:', err.message);
46
+ await new Promise((r) => setTimeout(r, 2000));
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SSO token retrieval for Okta and Azure AD.
3
+ * Uses OAuth2 ROPC (Resource Owner Password Credentials) flow to get access tokens
4
+ * without browser-based login.
5
+ *
6
+ * Note: ROPC is deprecated in OAuth 2.1 but still supported by Okta and Azure AD
7
+ * for service accounts and automated testing scenarios.
8
+ */
9
+
10
+ export type SsoProvider = 'okta' | 'azure_ad' | 'generic_oidc';
11
+
12
+ export interface SsoConfig {
13
+ provider: SsoProvider;
14
+ /** Okta domain (e.g. 'mycompany.okta.com') or Azure tenant ID */
15
+ domain: string;
16
+ /** OAuth2 client ID */
17
+ clientId: string;
18
+ /** OAuth2 client secret (for client_credentials or confidential ROPC) */
19
+ clientSecret?: string;
20
+ /** Scopes to request (space-separated). Default: 'openid profile' */
21
+ scope?: string;
22
+ }
23
+
24
+ /**
25
+ * Get an access token using ROPC flow (username + password → token).
26
+ * Returns the access token string.
27
+ */
28
+ export async function getSsoToken(
29
+ config: SsoConfig,
30
+ username: string,
31
+ password: string
32
+ ): Promise<string> {
33
+ let tokenUrl: string;
34
+ const params: Record<string, string> = {
35
+ grant_type: 'password',
36
+ username,
37
+ password,
38
+ scope: config.scope ?? 'openid profile',
39
+ client_id: config.clientId,
40
+ };
41
+
42
+ if (config.clientSecret) {
43
+ params.client_secret = config.clientSecret;
44
+ }
45
+
46
+ switch (config.provider) {
47
+ case 'okta':
48
+ tokenUrl = `https://${config.domain}/oauth2/v1/token`;
49
+ break;
50
+ case 'azure_ad':
51
+ // domain here is the tenant ID (e.g. XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX or 'mycompany.onmicrosoft.com')
52
+ tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;
53
+ break;
54
+ case 'generic_oidc':
55
+ tokenUrl = config.domain.startsWith('http') ? config.domain : `https://${config.domain}/oauth2/token`;
56
+ break;
57
+ default:
58
+ throw new Error(`Unknown SSO provider: ${config.provider}`);
59
+ }
60
+
61
+ const res = await fetch(tokenUrl, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
64
+ body: new URLSearchParams(params).toString(),
65
+ });
66
+
67
+ if (!res.ok) {
68
+ const body = await res.text().catch(() => res.statusText);
69
+ throw new Error(`SSO token request failed (${res.status}): ${body}`);
70
+ }
71
+
72
+ const data = await res.json() as { access_token?: string; error?: string; error_description?: string };
73
+ if (data.error) throw new Error(`SSO error: ${data.error} — ${data.error_description}`);
74
+ if (!data.access_token) throw new Error('SSO response missing access_token');
75
+
76
+ return data.access_token;
77
+ }
78
+
79
+ /**
80
+ * Get a client credentials token (no user — for service accounts).
81
+ */
82
+ export async function getSsoClientCredentialsToken(
83
+ config: SsoConfig
84
+ ): Promise<string> {
85
+ if (!config.clientSecret) throw new Error('clientSecret required for client credentials flow');
86
+
87
+ let tokenUrl: string;
88
+ switch (config.provider) {
89
+ case 'okta':
90
+ tokenUrl = `https://${config.domain}/oauth2/v1/token`;
91
+ break;
92
+ case 'azure_ad':
93
+ tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;
94
+ break;
95
+ default:
96
+ tokenUrl = `https://${config.domain}/oauth2/token`;
97
+ }
98
+
99
+ const res = await fetch(tokenUrl, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
102
+ body: new URLSearchParams({
103
+ grant_type: 'client_credentials',
104
+ client_id: config.clientId,
105
+ client_secret: config.clientSecret,
106
+ scope: config.scope ?? 'openid profile',
107
+ }).toString(),
108
+ });
109
+
110
+ if (!res.ok) {
111
+ const body = await res.text().catch(() => res.statusText);
112
+ throw new Error(`SSO client credentials failed (${res.status}): ${body}`);
113
+ }
114
+
115
+ const data = await res.json() as { access_token?: string; error?: string; error_description?: string };
116
+ if (data.error) throw new Error(`SSO error: ${data.error} — ${data.error_description}`);
117
+ if (!data.access_token) throw new Error('SSO response missing access_token');
118
+
119
+ return data.access_token;
120
+ }
@@ -0,0 +1,191 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ // Gaussian random using Box-Muller transform
4
+ function gaussian(mean: number, sigma: number): number {
5
+ const u1 = Math.random();
6
+ const u2 = Math.random();
7
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
8
+ return mean + z * sigma;
9
+ }
10
+
11
+ export async function humanDelay(min = 80, max = 450): Promise<void> {
12
+ const mean = (min + max) / 2;
13
+ const sigma = (max - min) / 6;
14
+ const delay = Math.max(min, Math.min(max, gaussian(mean, sigma)));
15
+ await new Promise(r => setTimeout(r, Math.round(delay)));
16
+ }
17
+
18
+ export async function simulateMouseMovement(page: Page, targetX: number, targetY: number): Promise<void> {
19
+ try {
20
+ const mouse = page.mouse;
21
+ const steps = 8 + Math.floor(Math.random() * 8);
22
+ // Generate Bezier control points with slight perpendicular offset
23
+ const startX = targetX - 100 + Math.random() * 200;
24
+ const startY = targetY - 100 + Math.random() * 200;
25
+ const cpX = (startX + targetX) / 2 + (Math.random() - 0.5) * 80;
26
+ const cpY = (startY + targetY) / 2 + (Math.random() - 0.5) * 80;
27
+
28
+ for (let i = 0; i <= steps; i++) {
29
+ const t = i / steps;
30
+ // Quadratic Bezier: B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
31
+ const x = (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * cpX + t * t * targetX;
32
+ const y = (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * cpY + t * t * targetY;
33
+ await mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2);
34
+ if (i < steps) await humanDelay(8, 25);
35
+ }
36
+ // Small overshoot correction
37
+ await mouse.move(targetX + (Math.random() - 0.5) * 4, targetY + (Math.random() - 0.5) * 4);
38
+ await humanDelay(30, 80);
39
+ await mouse.move(targetX, targetY);
40
+ } catch { /* non-fatal */ }
41
+ }
42
+
43
+ export async function simulateMouseHover(page: Page, selector: string): Promise<void> {
44
+ try {
45
+ const el = await page.$(selector);
46
+ if (!el) return;
47
+ const box = await el.boundingBox();
48
+ if (!box) return;
49
+ const cx = box.x + box.width / 2;
50
+ const cy = box.y + box.height / 2;
51
+ await simulateMouseMovement(page, cx, cy);
52
+ await humanDelay(100, 300);
53
+ } catch { /* non-fatal */ }
54
+ }
55
+
56
+ export async function simulateClick(page: Page, selector: string): Promise<void> {
57
+ try {
58
+ await simulateMouseHover(page, selector);
59
+ await humanDelay(50, 150);
60
+ await page.click(selector, { delay: Math.round(gaussian(80, 30)) });
61
+ } catch { /* non-fatal */ }
62
+ }
63
+
64
+ export async function injectFingerprintOverrides(page: Page): Promise<void> {
65
+ await page.addInitScript(() => {
66
+ // Canvas noise — subtle per-pixel perturbation
67
+ const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
68
+ HTMLCanvasElement.prototype.toDataURL = function(this: HTMLCanvasElement, ...args: any[]) {
69
+ const ctx = this.getContext('2d');
70
+ if (ctx) {
71
+ const imageData = ctx.getImageData(0, 0, this.width, this.height);
72
+ for (let i = 0; i < imageData.data.length; i += 4) {
73
+ imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (Math.random() < 0.01 ? (Math.random() > 0.5 ? 1 : -1) : 0)));
74
+ }
75
+ ctx.putImageData(imageData, 0, 0);
76
+ }
77
+ return origToDataURL.apply(this, args as [type?: string, quality?: number]);
78
+ };
79
+
80
+ // WebGL parameter spoofing
81
+ const origGetParam = WebGLRenderingContext.prototype.getParameter;
82
+ WebGLRenderingContext.prototype.getParameter = function(this: WebGLRenderingContext, parameter: number) {
83
+ const UNMASKED_VENDOR_WEBGL = 0x9245;
84
+ const UNMASKED_RENDERER_WEBGL = 0x9246;
85
+ if (parameter === UNMASKED_VENDOR_WEBGL) return 'Intel Inc.';
86
+ if (parameter === UNMASKED_RENDERER_WEBGL) return 'Intel Iris OpenGL Engine';
87
+ return origGetParam.call(this, parameter);
88
+ };
89
+
90
+ // Hardware concurrency — pick from common values
91
+ const cpuCounts = [4, 6, 8, 8];
92
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
93
+ get: () => cpuCounts[Math.floor(Math.random() * cpuCounts.length)],
94
+ configurable: true,
95
+ });
96
+
97
+ // Device memory
98
+ Object.defineProperty(navigator, 'deviceMemory', {
99
+ get: () => [4, 8][Math.floor(Math.random() * 2)],
100
+ configurable: true,
101
+ });
102
+ }).catch(() => {});
103
+ }
104
+
105
+ export async function infiniteScrollToBottom(
106
+ page: Page,
107
+ opts: { maxScrolls?: number; scrollDelay?: number; stabilizeMs?: number } = {}
108
+ ): Promise<{ scrollCount: number; heightReached: number }> {
109
+ const { maxScrolls = 50, scrollDelay = 400, stabilizeMs = 600 } = opts;
110
+ let scrollCount = 0;
111
+ let sameHeightCount = 0;
112
+ let lastHeight = 0;
113
+
114
+ try {
115
+ while (scrollCount < maxScrolls) {
116
+ const currentHeight: number = await page.evaluate(() => document.body.scrollHeight).catch(() => 0);
117
+ if (currentHeight === lastHeight) {
118
+ sameHeightCount++;
119
+ if (sameHeightCount >= 3) break; // no new content
120
+ } else {
121
+ sameHeightCount = 0;
122
+ lastHeight = currentHeight;
123
+ }
124
+
125
+ const scrollBy = 500 + Math.floor(Math.random() * 300);
126
+ await page.evaluate((by: number) => window.scrollBy(0, by), scrollBy).catch(() => {});
127
+ await humanDelay(scrollDelay - 100, scrollDelay + 200);
128
+
129
+ // Wait for potential lazy-loaded content
130
+ await page.waitForTimeout(stabilizeMs).catch(() => {});
131
+ scrollCount++;
132
+ }
133
+ } catch { /* non-fatal */ }
134
+
135
+ return { scrollCount, heightReached: lastHeight };
136
+ }
137
+
138
+ export async function extractIframeContent(page: Page): Promise<Array<{ src: string; content: string; markdown: string }>> {
139
+ const results: Array<{ src: string; content: string; markdown: string }> = [];
140
+ try {
141
+ const iframeSrcs: string[] = await page.evaluate(() =>
142
+ Array.from(document.querySelectorAll('iframe')).map(f => f.getAttribute('src') ?? f.getAttribute('data-src') ?? '')
143
+ ).catch(() => []);
144
+
145
+ for (const frame of page.frames()) {
146
+ const src = frame.url();
147
+ if (!src || src === 'about:blank' || src === page.url()) continue;
148
+ try {
149
+ const content = await frame.content().catch(() => '');
150
+ if (!content || content.length < 50) continue;
151
+ // Simple text extraction from iframe HTML
152
+ const text = content.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
153
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
154
+ .replace(/<[^>]+>/g, ' ')
155
+ .replace(/\s+/g, ' ')
156
+ .trim();
157
+ results.push({ src, content: content.slice(0, 10_000), markdown: text.slice(0, 5_000) });
158
+ } catch { /* skip this frame */ }
159
+ }
160
+ } catch { /* non-fatal */ }
161
+ return results;
162
+ }
163
+
164
+ export async function pierceShadowDom(page: Page): Promise<string> {
165
+ try {
166
+ return await page.evaluate(() => {
167
+ const parts: string[] = [];
168
+ function walkShadow(root: Element | ShadowRoot): void {
169
+ const shadow = (root as any).shadowRoot;
170
+ if (shadow) {
171
+ parts.push(shadow.innerHTML ?? '');
172
+ for (const child of [...shadow.querySelectorAll('*')]) walkShadow(child);
173
+ }
174
+ for (const child of Array.from(root.querySelectorAll(':not(script):not(style)'))) {
175
+ if ((child as any).shadowRoot) walkShadow(child);
176
+ }
177
+ }
178
+ walkShadow(document.body);
179
+ return parts
180
+ .join('\n')
181
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
182
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
183
+ .replace(/<[^>]+>/g, ' ')
184
+ .replace(/\s+/g, ' ')
185
+ .trim()
186
+ .slice(0, 10_000);
187
+ }).catch(() => '');
188
+ } catch {
189
+ return '';
190
+ }
191
+ }
@@ -0,0 +1,122 @@
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
+
13
+ import { randomBytes } from 'node:crypto';
14
+
15
+ export interface Span {
16
+ traceId: string;
17
+ spanId: string;
18
+ parentSpanId?: string;
19
+ name: string;
20
+ startTimeMs: number;
21
+ attributes: Record<string, string | number | boolean>;
22
+ }
23
+
24
+ export interface FinishedSpan extends Span {
25
+ endTimeMs: number;
26
+ durationMs: number;
27
+ status: 'ok' | 'error';
28
+ error?: string;
29
+ }
30
+
31
+ // Active trace context — per async execution context (simplified: per-process global)
32
+ let currentTraceId: string | null = null;
33
+ let currentSpanId: string | null = null;
34
+
35
+ function genId(bytes: number): string {
36
+ return randomBytes(bytes).toString('hex');
37
+ }
38
+
39
+ /** Start a new span. If a trace is already active, the new span is a child. */
40
+ export function startSpan(name: string, attributes: Record<string, string | number | boolean> = {}): Span {
41
+ const traceId = currentTraceId ?? genId(16);
42
+ const spanId = genId(8);
43
+ const parentSpanId = currentSpanId ?? undefined;
44
+
45
+ if (!currentTraceId) currentTraceId = traceId;
46
+ currentSpanId = spanId;
47
+
48
+ return { traceId, spanId, parentSpanId, name, startTimeMs: Date.now(), attributes };
49
+ }
50
+
51
+ /** End a span and emit it as a structured log line. */
52
+ export function endSpan(span: Span, extraAttributes: Record<string, string | number | boolean> = {}, error?: Error): FinishedSpan {
53
+ const endTimeMs = Date.now();
54
+ const durationMs = endTimeMs - span.startTimeMs;
55
+ const finished: FinishedSpan = {
56
+ ...span,
57
+ attributes: { ...span.attributes, ...extraAttributes },
58
+ endTimeMs,
59
+ durationMs,
60
+ status: error ? 'error' : 'ok',
61
+ error: error?.message,
62
+ };
63
+
64
+ // Reset context when root span ends
65
+ if (!span.parentSpanId) {
66
+ currentTraceId = null;
67
+ currentSpanId = span.parentSpanId ?? null;
68
+ } else {
69
+ currentSpanId = span.parentSpanId ?? null;
70
+ }
71
+
72
+ // Emit as structured JSON — compatible with Datadog/Grafana log parsing
73
+ console.log(JSON.stringify({
74
+ ts: new Date(endTimeMs).toISOString(),
75
+ level: error ? 'error' : 'info',
76
+ msg: `span:${finished.name}`,
77
+ trace_id: finished.traceId,
78
+ span_id: finished.spanId,
79
+ parent_span_id: finished.parentSpanId,
80
+ duration_ms: durationMs,
81
+ status: finished.status,
82
+ error: finished.error,
83
+ ...finished.attributes,
84
+ }));
85
+
86
+ return finished;
87
+ }
88
+
89
+ /** Convenience: wrap an async function in a span. */
90
+ export async function withSpan<T>(
91
+ name: string,
92
+ attributes: Record<string, string | number | boolean>,
93
+ fn: (span: Span) => Promise<T>,
94
+ ): Promise<T> {
95
+ const span = startSpan(name, attributes);
96
+ try {
97
+ const result = await fn(span);
98
+ endSpan(span);
99
+ return result;
100
+ } catch (err: any) {
101
+ endSpan(span, {}, err);
102
+ throw err;
103
+ }
104
+ }
105
+
106
+ /** Counter metrics — increment and log on flush. */
107
+ const counters: Map<string, number> = new Map();
108
+
109
+ export function increment(metric: string, value = 1, tags: Record<string, string> = {}): void {
110
+ const key = metric + (Object.keys(tags).length ? ':' + Object.entries(tags).map(([k, v]) => `${k}=${v}`).join(',') : '');
111
+ counters.set(key, (counters.get(key) ?? 0) + value);
112
+ }
113
+
114
+ /** Flush counters as a structured log — call periodically (e.g. at job end). */
115
+ export function flushMetrics(): Record<string, number> {
116
+ const snapshot = Object.fromEntries(counters);
117
+ if (Object.keys(snapshot).length > 0) {
118
+ console.log(JSON.stringify({ ts: new Date().toISOString(), level: 'info', msg: 'metrics:flush', ...snapshot }));
119
+ counters.clear();
120
+ }
121
+ return snapshot;
122
+ }