{"_id":"@edraj/sauron-browser","_rev":"8-a6e7618af1cab21a97e63aa6a525d62a","name":"@edraj/sauron-browser","dist-tags":{"latest":"1.8.0"},"versions":{"1.0.0":{"name":"@edraj/sauron-browser","version":"1.0.0","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"AGPL-3.0-only","_id":"@edraj/sauron-browser@1.0.0","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"b09a21405933bdb2a3e987e4f9ac251610d33a1c","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.0.0.tgz","fileCount":10,"integrity":"sha512-ORlijIyw2rJZnlk8unZBT1csJqbMASvnsUDGYwwEcMqYr0Dw8W1gUatff3TDoxeGYi19B0Nr6VoGUFWS52oemg==","signatures":[{"sig":"MEUCIH2u0jpaB9AeWwlS6pkJ/eFMJnFHCZMD/58goiRyfpvNAiEA9TB2x6zPsSS5wrOebza3CP+RQOQpRmSU0/HVvrm8zdw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":507084},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"8580ea5e9bc6358a419a3f9afee401e4fac46e48","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.7","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.2","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.0.0_1785189824787_0.5276553664299277","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@edraj/sauron-browser","version":"1.3.0","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"AGPL-3.0-only","_id":"@edraj/sauron-browser@1.3.0","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"3a642a0aa7a6066541754cf7dd18a32dfb847424","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.3.0.tgz","fileCount":10,"integrity":"sha512-nX/tslMsHv/h4ObKYpydmwb/A2KIIRGKrHOHcOkjkBelMnwnzkwc1xfvO+ku3NSN8IVBfcEJz6FLRSPbXTYPBA==","signatures":[{"sig":"MEYCIQDRLsmd9VPk22s6ZoBl9vf7z7AqAMUYsdqBaG8AVvM49AIhAKCzr53NOwF9LGV0eVXuE872WqnvIk8xESq9a5QDLONc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":608216},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"ffd318f67e5336151327f5ab34838c41f55d4d97","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.7","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.2","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.3.0_1785434940814_0.9207611557950395","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@edraj/sauron-browser","version":"1.4.0","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"AGPL-3.0-only","_id":"@edraj/sauron-browser@1.4.0","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"21bb6df755b5adf71778518e2e86969e835fbba9","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.4.0.tgz","fileCount":10,"integrity":"sha512-6hkQmwJKBusTfq3j8BAeBG88ueKjy83/zXsUacXBjSoUiJ9aTHQyBkS5DWal48KNPAtS22YBDHQ/R6xDEpnf5A==","signatures":[{"sig":"MEUCIQCuPkzdhne5wx4mEWt9PtNsUFgsGtkaciQEXOQSSRl7zQIgFjWBXi4SUOoZ2MNW49iNEeFOeFvpQWB++2MqrRI+NDo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":637079},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"7d29f681c2afea1c42903a93472dc7f0e441e99c","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.8","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.4.0_1786407144934_0.985936540624454","host":"s3://npm-registry-packages-npm-production"}},"1.4.1":{"name":"@edraj/sauron-browser","version":"1.4.1","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"AGPL-3.0-only","_id":"@edraj/sauron-browser@1.4.1","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"7e828b2031f275babd3fd87fb0258b7a4209334a","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.4.1.tgz","fileCount":10,"integrity":"sha512-10AfPVywprwXrT5PPU18+s3d0bNVFv6QAbBlrogAkGZ21uuUHoGZmvRtQFuLcxGH5y2LYFC9+W7c6++ZvEDsuA==","signatures":[{"sig":"MEUCIQDoVrHl9uNZaX15vs53Y4BlHQ06b0Hkgm7aFZ8+LjFAJgIgEqd+ZaZIBkcZWY0geENAKM3v+UCgaMizqmZMbEnrWVc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":687203},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"4cd7f147135bc12d62cfa3f22809c7de0bfe7e9c","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.8","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.4.1_1786725399872_0.6848624668063048","host":"s3://npm-registry-packages-npm-production"}},"1.5.0":{"name":"@edraj/sauron-browser","version":"1.5.0","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"AGPL-3.0-only","_id":"@edraj/sauron-browser@1.5.0","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"faeaf8b59d0b5f2ef5149e9985afaf448dc8377f","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.5.0.tgz","fileCount":10,"integrity":"sha512-gMS+Xhewkov4p191Ff3eW9mGW7KJcQOFc+ib+butxPFtcpuFvvb2Qjq3tLSctGmPvMffzkkyMnJeEBFoQygMFw==","signatures":[{"sig":"MEYCIQDcowTxu17p+j7NjZI29MgL34yzZmjEMjuVKXypCJriJAIhAMxRgMajAzosfZR7VULtNnMxAQg63yH10a6dg5yuAG5X","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":713826},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"a1399a533c991b8732afcdda614d42dca2801151","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.8","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.5.0_1786919146018_0.259695679671214","host":"s3://npm-registry-packages-npm-production"}},"1.6.0":{"name":"@edraj/sauron-browser","version":"1.6.0","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"AGPL-3.0-only","_id":"@edraj/sauron-browser@1.6.0","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"807e44e0e10080613eab62dea0418615c49358d0","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.6.0.tgz","fileCount":10,"integrity":"sha512-A7/pcAyqAZd6DfNVhlNjM24mblWAG99w7beMUQofBy7iweGu16pcWUa6reCWWkyymnNmZwcP1+eVpW3RFEYpsg==","signatures":[{"sig":"MEUCIQDXse7PAWoIsMueDRGYBFeGmWS968Z7MJmXXFqNA5Xh5AIgauIVWenCv7kcUs1cj3K+JZXMpFaBPKjdIBdvw1yfDrI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":718121},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"1ce04400fc7d287ff017c4a437b88269a3a32ec7","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.8","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.6.0_1787003407928_0.41920504656211066","host":"s3://npm-registry-packages-npm-production"}},"1.7.0":{"name":"@edraj/sauron-browser","version":"1.7.0","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"license":"LGPL-3.0-only","_id":"@edraj/sauron-browser@1.7.0","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"6ac4a303ec8ac857eab043438fa35960dc678643","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.7.0.tgz","fileCount":11,"integrity":"sha512-heSlGPcP9t5shS79PSDdBAliosVDa2ztV36oQSBAsax29lQ1pojEJhVSfOCRQiHiL5isudKR0u0U5bdPAYQTKg==","signatures":[{"sig":"MEQCIHof3a5U+rT4tnJxm1ZiwVTXUrNvjLNPg2ADhvxWkpgVAiB4AGyVtxYeX/5WEdVdZl3QWcOSdnfw96t9mqLnM3ADyg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQC4XGtzZfOnMepr+QIdf1Rn/uw+VRkPuAF+bzLRpJagEAIhAK8JjboZLvmdkUmXpUrYsFYbU6Nh28UL3bqZzS06E+yn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":728124},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"4c05b751ea307beb12d66bb02af5a368d56dc9cd","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.8","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","vitest":"^3.2.0","typescript":"^5.9.0"},"_npmOperationalInternal":{"tmp":"tmp/sauron-browser_1.7.0_1789381541853_0.1275102614402106","host":"s3://npm-registry-packages-npm-production"}},"1.8.0":{"_id":"@edraj/sauron-browser@1.8.0","bugs":{"url":"https://github.com/edraj/sauron/issues"},"dist":{"shasum":"d1583b32382432c906c46408fbe2cbf42b9072f0","tarball":"https://registry.npmjs.org/@edraj/sauron-browser/-/sauron-browser-1.8.0.tgz","fileCount":14,"integrity":"sha512-NNgTV4kaOYQCkxRycFquTFk4ndORrB9glurEMyI/HixDO0tka8zrHytPqeenlm1IPKNegR/DoQakubyAwi+9Wg==","signatures":[{"sig":"MEUCIQDgqzVnCj4esRF8CHKa6nn3Ukgn0W8OcGvlX0Ax/zw1bQIgODCi5PVu8Vq+wgUCFGbnR7yo2JK4Eh7zJ9aoQBvYj60=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEHQ1RJ5NF8xOIB7msNhl+2fiNl3WzXra6xPo2u8ke9MAiEAw47Cv9E05ROMfo45QdvUqGhkZarjyrFpdu0s0rI16WM="}],"unpackedSize":1135191},"main":"./dist/index.cjs","name":"@edraj/sauron-browser","type":"module","types":"./dist/index.d.ts","unpkg":"dist/sauron.min.js","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"4ae6bb693808677eeb9f35191e1d48ff4b7f93e9","license":"LGPL-3.0-only","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup && node scripts/build-global.mjs","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm run test && npm run build"},"version":"1.8.0","_npmUser":{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},"homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","jsdelivr":"dist/sauron.min.js","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"_npmVersion":"10.9.8","description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","directories":{},"maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"sideEffects":false,"_nodeVersion":"22.23.1","dependencies":{"fflate":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","acorn":"^8.18.0","vitest":"^3.2.0","esbuild":"^0.27.7","@swc/core":"^1.16.2","typescript":"^5.9.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sauron-browser_1.8.0_1790499840896_0.48518199284158703"}}},"time":{"created":"2026-07-27T22:03:44.543Z","modified":"2026-09-27T09:04:01.216Z","1.0.0":"2026-07-27T22:03:44.949Z","1.3.0":"2026-07-30T18:09:00.959Z","1.4.0":"2026-08-11T00:12:25.149Z","1.4.1":"2026-08-14T16:36:40.065Z","1.5.0":"2026-08-16T22:25:46.196Z","1.6.0":"2026-08-17T21:50:08.074Z","1.7.0":"2026-09-14T10:25:41.983Z","1.8.0":"2026-09-27T09:04:01.042Z"},"bugs":{"url":"https://github.com/edraj/sauron/issues"},"license":"LGPL-3.0-only","homepage":"https://github.com/edraj/sauron/tree/main/sdks/js#readme","keywords":["sauron","error-tracking","analytics","observability","monitoring","sdk","browser"],"repository":{"url":"git+https://github.com/edraj/sauron.git","type":"git","directory":"sdks/js"},"description":"Sauron browser SDK: automatic error reporting + product analytics for the web.","maintainers":[{"name":"ms_splimter","email":"merah.soheyb@gmail.com"},{"name":"kefahi","email":"kefah.issa@gmail.com"}],"readme":"# @edraj/sauron-browser\n\nClient-side SDK for **Sauron** — error reporting and product analytics for the\nbrowser in one small package. It runs in the page (or any browser-like host) and\nposts a canonical JSON envelope to the Sauron ingest gateway. For a Node.js\nprocess — an API server, a worker, a CLI — use the server SDK\n[`@edraj/sauron-node`](../node) instead; this package assumes browser globals\n(`window`, `document`, `localStorage`) and ships a public, write-only DSN key.\n\n- Auto-instruments `window.onerror`, `onunhandledrejection`, `console`, DOM\n  clicks, `fetch`, `XMLHttpRequest`, and SPA History navigations out of the box.\n- Opt-in performance transactions (navigation timing, per-`fetch` HTTP spans,\n  SPA route spans) and opt-in screen tracking.\n- Batches, gzips, retries with jitter, and parks failed envelopes in a\n  `localStorage` queue that drains on the next page load or `online` event.\n- One runtime dependency (`fflate`, lazily imported only as a gzip fallback).\n- Ships ESM + CJS + type declarations, `sideEffects: false`, tree-shakeable —\n  plus a `<script>` build that defines `window.Sauron`, and an ES5 variant of it\n  for Google Tag Manager.\n\n## Install\n\n```bash\nnpm install @edraj/sauron-browser\n```\n\nNode >= 18 is required for the build/test tooling (`engines.node`). The shipped\nbundle targets ES2020 and needs no polyfills in evergreen browsers.\n\nNo bundler? See [Script tag & Google Tag Manager](#script-tag--google-tag-manager).\n\n## Quick start\n\n```ts\nimport { Sauron } from '@edraj/sauron-browser';\n\nSauron.init({\n  dsn: 'https://pk_test@ingest.example.com/42',\n  release: 'web@1.4.2',\n});\n\nSauron.identify('u_123', { plan: 'pro' });\nSauron.track('checkout_completed', { cart_value: 42.5 });\n\ntry {\n  doRiskyThing();\n} catch (err) {\n  Sauron.captureException(err);\n}\n\n// Optional: force delivery now instead of waiting for the 5 s flush tick.\nawait Sauron.flush(2000);\n```\n\nUncaught errors and unhandled rejections need no code at all — `init()` installs\nthe global handlers.\n\n## Script tag & Google Tag Manager\n\nFor a page with no build step, such as a Google Tag Manager Custom HTML tag or a\nCMS footer, the package ships two self-contained files. Each defines one global,\n`window.Sauron`, holding everything the module exports (`Sauron.init`,\n`Sauron.track`, `Sauron.captureException`, …, `Sauron.SDK_VERSION`):\n\n| File | Syntax | For |\n| --- | --- | --- |\n| `dist/sauron.min.js` | ES2020 | Loading from a CDN with `<script src>`. |\n| `dist/sauron.es5.min.js` | ES5 | Pasting inline into a host that only accepts ES5. GTM Custom HTML tags reject arrow functions, classes and `const`. |\n\n**From the CDN.** jsDelivr and unpkg serve every published version:\n\n```html\n<script src=\"https://cdn.jsdelivr.net/npm/@edraj/sauron-browser@1.8.0/dist/sauron.min.js\"></script>\n<script>\n  Sauron.init({ dsn: 'https://pk_test@ingest.example.com/42', release: 'web@1.4.2' });\n</script>\n```\n\nPin the exact version. An exact-version URL never changes, while `@1` moves to\neach new release the next time the CDN refreshes it.\n\nTo make the browser refuse a file that was altered on the CDN, add Subresource\nIntegrity: `integrity=\"sha384-…\"` and `crossorigin=\"anonymous\"` on the tag (in\nthe GTM snippet below, `script.integrity` and `script.crossOrigin`). It needs an\nexact-version URL. Get the hash of a published file with:\n\n```bash\ncurl -s https://cdn.jsdelivr.net/npm/@edraj/sauron-browser@1.8.0/dist/sauron.min.js | openssl dgst -sha384 -binary | openssl base64 -A\n```\n\n**In Google Tag Manager**, create a Custom HTML tag fired by the\n*Initialization - All Pages* trigger: once per page, as early as GTM allows\n(errors thrown before the tag runs are not captured). Either load the CDN file\nfrom it with this loader:\n\n<!-- test/global-bundle.test.ts runs this snippet against the built file. -->\n```html\n<script>\n  (function (w, d, src, options) {\n    var s = w.Sauron;\n    if (!s) {\n      // A stand-in until the file loads: each call is queued in s.q, and the\n      // SDK replays the queue when it arrives.\n      s = w.Sauron = { q: [] };\n      ('init captureException captureMessage track trackTransaction identify ' +\n        'addBreadcrumb setUser reset setTag setTags setContext setExtra ' +\n        'setScreen startWorkflow endWorkflow cancelWorkflow flush close')\n        .split(' ')\n        .forEach(function (name) {\n          s[name] = function () {\n            s.q.push([name, Array.prototype.slice.call(arguments)]);\n          };\n        });\n      // Uncaught errors and unhandled rejections are queued too (up to 100\n      // entries), until the SDK's own handlers take over.\n      var onError = function (event) {\n        if (w.Sauron !== s) {\n          w.removeEventListener('error', onError);\n          w.removeEventListener('unhandledrejection', onError);\n        } else if (s.q.length < 100) {\n          s.q.push(['$' + event.type, [event]]);\n        }\n      };\n      w.addEventListener('error', onError);\n      w.addEventListener('unhandledrejection', onError);\n      var script = d.createElement('script');\n      script.async = true;\n      script.src = src;\n      d.head.appendChild(script);\n    }\n    s.init(options);\n  })(window, document, 'https://cdn.jsdelivr.net/npm/@edraj/sauron-browser@1.8.0/dist/sauron.min.js', {\n    dsn: 'https://pk_test@ingest.example.com/42',\n    release: 'web@1.4.2'\n  });\n</script>\n```\n\nGTM only checks the code inside the tag, which is ES5 here. The file it loads\ncan be ES2020.\n\nOr, with no CDN, paste the whole of `dist/sauron.es5.min.js` into the tag:\n\n```html\n<script>\n  /* paste the contents of dist/sauron.es5.min.js here */\n</script>\n<script>\n  Sauron.init({ dsn: 'https://pk_test@ingest.example.com/42', release: 'web@1.4.2' });\n</script>\n```\n\nThe pasted file counts against the container's size limit (GTM caps a container\nat 200 KB). The loader tag is about 1.5 KB.\n\n**Calling it from other tags.** Any tag that fires after the Sauron tag can call\nthe SDK directly:\n\n```html\n<script>\n  Sauron.track('purchase', { value: {{Order Value}} });\n</script>\n```\n\nWith the loader, a call made before the file has arrived is queued and replayed\nwhen it loads: `init()` first, then the rest in the order they were made. Until\nthen, those calls return `undefined` (no `flush()` promise, no\n`startWorkflow()` result), and `getScreen()`, `getWorkflow()` and `getClient()`\ndo not exist yet.\n\nThe loader also queues the page's uncaught errors and unhandled rejections,\nfrom the moment the tag runs until the file arrives (up to 100 queue entries).\nThe SDK reports them when it loads, as its own handlers would have, and its\nhandlers take over from there.\n\nAnything queued is timestamped when it replays, not when it happened. A tag\nthat fires before the Sauron tag, such as a *Consent Initialization* tag, finds\nno `window.Sauron` at all, so guard it: `if (window.Sauron) …`.\n\n- The ES5 file is ES5 *syntax*, not an ES5 runtime. It still needs `Promise`,\n  `fetch`, `URL`, `TextEncoder` and `globalThis`, so it does not make the SDK\n  run in Internet Explorer.\n- Loading either file twice (a tag that also fires on history changes) keeps the\n  first copy. Calling `init()` again re-initializes it cleanly.\n- Neither file creates any global other than `Sauron`.\n\n## Configuration\n\n`init(options)` takes an `InitOptions` object. `dsn` and `release` are required;\nanything else missing falls back to the default below (resolved in\n`resolveOptions()`).\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `dsn` | `string` | — **(required)** | `https://<public_key>@<host>/<environment_id>`. A non-string or empty value throws `Error`; a malformed URL throws `DsnError`. |\n| `release` | `string` | — **(required)** | The app version this build reports as. Trimmed, then stamped on `header.release`; the part after the last `@` also becomes `context.app.version` (`web@1.4.2` → `1.4.2`). A missing, non-string, empty or whitespace-only value throws `Error`. |\n| `sampleRate` | `number` | `1` | Fraction of **error items** sent, clamped into `[0, 1]`. Applies to `captureException`, `captureMessage` and the global handlers only — events, identifies and transactions are never sampled. |\n| `maxBreadcrumbs` | `number` | `50` | Ring-buffer size; oldest entries are evicted first. Negative values are treated as `0`, which disables breadcrumbs entirely. |\n| `beforeSend` | `(item: EnvelopeItem, hint?: Hint) => EnvelopeItem \\| null` | `undefined` | Runs on **every** item type just before the transport. Return `null` to drop. If it throws, the original item is sent and a warning is logged in `debug` mode. |\n| `beforeBreadcrumb` | `(breadcrumb: Breadcrumb, hint?: Hint) => Breadcrumb \\| null` | `undefined` | Runs on every breadcrumb before it enters the buffer. Return `null` to drop. Throwing keeps the original. |\n| `transport` | `TransportOptions` | see below | Batching / queue tuning. |\n| `performance` | `boolean` | `false` | Opt-in performance auto-capture (see [Automatic instrumentation](#automatic-instrumentation)). Manual `trackTransaction()` works regardless. |\n| `screen` | `string` | `undefined` | Seeds the initial screen name. Seeding does **not** emit a `$screen` event — only a later `setScreen()` change does. |\n| `screenTracking` | `boolean` | `false` | Opt-in: set the screen to the new path on every SPA History navigation (which emits `$screen`). `setScreen()` works regardless. |\n| `tags` | `Record<string, string>` | `{}` | Default tags seeded into the global scope. |\n| `contexts` | `Record<string, Record<string, unknown>>` | `{}` | Default named context blocks seeded into the global scope. |\n| `extra` | `Record<string, unknown>` | `{}` | Default freeform values seeded into the global scope. |\n| `debug` | `boolean` | `false` | Log SDK diagnostics to `console` with a `[sauron]` prefix. |\n\n`TransportOptions`:\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `flushIntervalMs` | `number` | `5000` | Periodic flush cadence. A value `<= 0` disables the timer (you must call `flush()` yourself). |\n| `maxBatch` | `number` | `30` | Items per envelope before an eager flush. Clamped into `[1, 1000]` — 1000 is the server's per-envelope item limit. |\n| `maxQueueBytes` | `number` | `1048576` | Byte cap on the offline `localStorage` queue (1 MiB). Negative values are treated as `0`. |\n\nEvery option set at once:\n\n```ts\nimport { Sauron } from '@edraj/sauron-browser';\n\nSauron.init({\n  dsn: 'https://pk_test@ingest.example.com/42',\n  release: 'web@1.4.2',\n  sampleRate: 0.5,\n  maxBreadcrumbs: 100,\n  beforeSend(item, hint) {\n    // `exception` is optional — a `captureMessage` item has none, and carries\n    // its text in `item.message` instead.\n    if (item.type === 'error' && item.exception?.value?.includes('token=')) {\n      return null; // PII escape hatch\n    }\n    return item;\n  },\n  beforeBreadcrumb(crumb) {\n    return crumb.category === 'console' ? null : crumb;\n  },\n  transport: {\n    flushIntervalMs: 5000,\n    maxBatch: 30,\n    maxQueueBytes: 1048576,\n  },\n  performance: true,\n  screen: '/',\n  screenTracking: true,\n  tags: { tier: 'free' },\n  contexts: { deploy: { region: 'eu-west-1' } },\n  extra: { build: 'ci-42' },\n  debug: true,\n});\n```\n\n## Funnels\n\nFunnels track the conversion rate of users progressing through a defined sequence of steps. By tracking a unique event at each step, the Sauron dashboard can visualize where users drop off.\n\n```ts\n// 1. User arrives at the pricing page\nSauron.track('pricing_viewed');\n\n// 2. User clicks on a plan\nSauron.track('plan_selected', { plan: 'pro' });\n\n// 3. User successfully checks out\nSauron.track('checkout_completed', { plan: 'pro', value: 42.5 });\n```\n\n## User Journeys\n\nUser journeys track the broader path a user takes through your application. Combine `setScreen` (to track navigation) and `startWorkflow` (to group a multi-step process) to see exactly how a user reached an outcome or encountered an error.\n\n```ts\n// Update the screen when the user navigates\nSauron.setScreen('/onboarding/step1');\n\n// Start a workflow to group all subsequent events and errors\nSauron.startWorkflow('user_onboarding');\n\n// Track specific actions within the journey\nSauron.track('profile_photo_uploaded');\n\nSauron.setScreen('/onboarding/step2');\nSauron.track('preferences_saved');\n\n// End the workflow when the journey concludes\nSauron.endWorkflow();\n```\n\n## API reference\n\nEverything is exported both as a named function and as a member of the `Sauron`\nfacade (also the default export). The two are the same function:\n\n```ts\nimport { Sauron } from '@edraj/sauron-browser';          // facade\nimport Sauron from '@edraj/sauron-browser';              // default export\nimport { init, captureException } from '@edraj/sauron-browser'; // named\n```\n\nThe facade carries `init`, `captureException`, `captureMessage`, `track`,\n`trackTransaction`, `identify`, `addBreadcrumb`, `setUser`, `setTag`, `setTags`,\n`setContext`, `setExtra`, `setScreen`, `getScreen`, `startWorkflow`,\n`endWorkflow`, `cancelWorkflow`, `getWorkflow`, `flush`, `close` and\n`getClient`.\n\nBefore `init()` every capture, analytics and scope function is a silent no-op,\n`getScreen()`/`getWorkflow()` return `null`, `startWorkflow`/`endWorkflow`/\n`cancelWorkflow` resolve to `{ status: 'disabled' }`, and `flush()`/`close()`\nresolve to `false`. Nothing throws. After `close()` the capture and scope\nfunctions stay no-ops (the client is disabled) and the screen and active\nworkflow are reset to `null`.\n\nThe same disabled state is also reached **automatically, mid-session**, the\nmoment the gateway answers a delivery with `401`/`403` (a revoked or invalid\nDSN key) — no call to `close()`/`disable()` required. Check\n[`isEnabled()`](#sauronclient) rather than assuming the SDK is live just\nbecause you never explicitly disabled it.\n\n### `init(options)`\n\n```ts\nfunction init(options: InitOptions): SauronClient\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `options` | `InitOptions` | — (required) | See [Configuration](#configuration). |\n\nResolves defaults, parses the DSN, seeds `tags`/`contexts`/`extra` into the\nglobal scope, installs the integrations, starts the flush timer and drains the\noffline queue. Returns the live `SauronClient`.\n\nIdempotent: a second `init()` tears the previous client down first (restoring\nevery patched global, clearing the current screen) before installing a fresh\none. Throws `Error` when `dsn` is missing or not a string, when `release` is\nmissing or blank, and `DsnError` when the DSN itself is malformed.\n\n`release` is required as of v1.7.0 — `init()` throws without one.\n\n```ts\nconst client = Sauron.init({\n  dsn: 'https://pk_test@localhost:8081/1',\n  release: 'web@1.4.2',\n});\nclient.options.release;     // 'web@1.4.2'\nclient.dsn.projectId;       // '1'\n```\n\n### `captureException(err, hint?)`\n\n```ts\nfunction captureException(err: unknown, hint?: Hint): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `err` | `unknown` | — (required) | An `Error`, an error-like object (`name` + string `message`), a string, any object, or a primitive. Non-errors are reduced to `{type, value}` with an empty stack trace. |\n| `hint` | `Hint` | `undefined` | Per-call overrides, also forwarded to `beforeSend`. |\n\nRecognized `hint` keys:\n\n| Key | Type | Default | Description |\n| --- | --- | --- | --- |\n| `level` | `Level` | `'error'` | `'debug' \\| 'info' \\| 'warning' \\| 'error' \\| 'fatal'`. |\n| `mechanism` | `Mechanism` | `{ type: 'generic', handled: true }` | How the error reached the SDK. |\n| `fingerprint` | `string[] \\| null` | `null` | Overrides server-side grouping. |\n| `screen` | `string` | current screen | Screen stamped on this item. |\n| `event_id` | `string` | fresh UUID v4 | Correlation id for the report. |\n| `message` | `string` | `undefined` | Human summary alongside the exception. |\n| `tags` | `Record<string, string>` | `undefined` | Merged over scope tags (this call only). |\n| `contexts` | `Record<string, Record<string, unknown>>` | `undefined` | Merged over scope contexts (this call only). |\n| `extra` | `Record<string, unknown>` | `undefined` | Merged over scope extra (this call only). |\n\nAny other key is passed through to `beforeSend` untouched. `originalException`\nis always set to `err` on the hint the SDK hands to `beforeSend`. Returns\n`void`; the item is buffered, not sent synchronously.\n\n```ts\ntry {\n  await placeOrder(orderId);\n} catch (err) {\n  Sauron.captureException(err, {\n    level: 'fatal',\n    fingerprint: ['checkout', 'place-order'],\n    tags: { flow: 'checkout' },\n    contexts: { order: { id: orderId } },\n    extra: { retry_count: 2 },\n  });\n}\n```\n\n### `captureMessage(message, level?, hint?)`\n\n```ts\nfunction captureMessage(message: string, level?: Level, hint?: Hint): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `message` | `string` | — (required) | Becomes the item's `message`. |\n| `level` | `Level` | `'info'` | Severity. |\n| `hint` | `Hint` | `undefined` | Only `fingerprint`, `event_id`, `tags`, `contexts` and `extra` are read here — unlike `captureException`, `hint.level`, `hint.mechanism` and `hint.screen` are ignored, and `hint.message` no longer applies because the `message` argument already occupies that field. |\n\nEmits an error item that carries **no `exception` block at all** — a message is\nnot an exception — plus the current breadcrumb trail and the current screen.\nServer-side it groups on the message-fallback fingerprint\n(`message` + the message text, normalized), so distinct messages become distinct\nissues instead of piling into one bucket keyed by a synthetic exception type.\nCounts against `sampleRate` like any other error item. Returns `void`.\n\n> Through 1.3.0 this shipped `exception: { type: null, value: message }`. The\n> gateway's exception type is a non-nullable string, so that item failed to\n> deserialize and the whole envelope came back `400 invalid_envelope` — and since\n> a 400 is a non-retryable drop, **every other item batched with it (up to\n> `maxBatch`, default 30) was silently lost too**. If you have a `beforeSend`\n> hook or any code reading `item.exception` on message items, note that the field\n> is now absent and `item.message` carries the text.\n\n```ts\nSauron.captureMessage('payment provider returned a soft decline', 'warning', {\n  tags: { provider: 'stripe' },\n});\n```\n\n### `track(name, properties?, options?)`\n\n```ts\nfunction track(\n  name: string,\n  properties?: Record<string, unknown>,\n  options?: TrackOptions,\n): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | — (required) | Event name. The SDK itself emits one reserved name, `$screen`. |\n| `properties` | `Record<string, unknown>` | `{}` | Event properties, sent verbatim. |\n| `options` | `TrackOptions` | `{}` | Per-call metadata, see below. |\n\n`TrackOptions` (extends `CaptureOptions`):\n\n| Field | Type | Default | Description |\n| --- | --- | --- | --- |\n| `tags` | `Record<string, string>` | `undefined` | Merged over scope tags. |\n| `contexts` | `Record<string, Record<string, unknown>>` | `undefined` | Merged over scope contexts (per block name). |\n| `extra` | `Record<string, unknown>` | `undefined` | Merged over scope extra. |\n| `screen` | `string` | current screen | Screen stamped on this event only; does not change the current screen. |\n\nThe event carries `distinct_id` (the identified user id, else a lazily minted\n`anon_<uuid>`), the session id and the screen. Events are never sampled.\nReturns `void`.\n\n```ts\nSauron.track('checkout_completed', { cart_value: 42.5, currency: 'EUR' }, {\n  tags: { experiment: 'new-cart' },\n  contexts: { cart: { items: 3 } },\n  extra: { coupon: 'SUMMER' },\n  screen: '/checkout/confirm',\n});\n```\n\n### `identify(id, traits?)`\n\n```ts\nfunction identify(id: string, traits?: Record<string, unknown>): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `id` | `string` | — (required) | The distinct id (your user id). |\n| `traits` | `Record<string, unknown>` | `{}` | Traits stored on the scope user and sent on the identify item. |\n\nSets the scope user to `{ id, traits }` and emits an identify item whose\n`anonymous_id` is the previously minted anonymous id, or `null` when the session\nnever needed one. Note that this replaces the whole scope user — an `email` set\nearlier via `setUser()` is cleared; call `setUser()` after `identify()` if you\nneed it. Returns `void`.\n\n```ts\nSauron.identify('u_123', { plan: 'pro', signup_month: '2026-03' });\n```\n\n### `trackTransaction(input)`\n\n```ts\nfunction trackTransaction(input: TransactionInput): void\n```\n\n| Field of `input` | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | — (required) | Transaction name, e.g. `GET /api/orders`. |\n| `durationMs` | `number` | — (required) | Wall-clock span in milliseconds. |\n| `op` | `string` | `'custom'` | One of `'navigation' \\| 'http' \\| 'resource' \\| 'screen_load' \\| 'custom'`. Anything else is coerced to `'custom'`. |\n| `status` | `string \\| null` | `null` | Free-form outcome, e.g. `'ok'` / `'error'`. |\n| `httpMethod` | `string \\| null` | `null` | For `http` ops. |\n| `httpStatus` | `number \\| null` | `null` | For `http` ops. |\n| `url` | `string \\| null` | `null` | For `http` ops. |\n| `tags` | `Record<string, string>` | omitted | Indexed string→string labels. Filter with `@tag.key:value` on the Transactions page. |\n| `extra` | `Record<string, unknown>` | omitted | Freeform JSON — request body, response body, SQL text, row counts. Searchable with `extra.key:value`. |\n\nThe item is stamped with the current distinct id, session id and timestamp.\nNever sampled. Returns `void`.\n\n**`tags` and `extra` are per-call only.** Unlike `track()` and\n`captureException()`, a transaction does **not** inherit the scope:\n`setTag()` / `setExtra()` defaults are not merged in. Transactions are the\nhighest-volume signal a page emits — one per navigation and per fetch — so\ninheriting a global blob would write it onto every row.\n\n`extra` is serialized and capped at **16 KB** (`MAX_TRANSACTION_EXTRA_BYTES`).\nPast that the whole map is replaced with `{ _truncated: true, _bytes: N }` and\nthe dashboard says so on the row. The cap is not cosmetic: envelopes are\nbatched, and one oversized body would push the whole envelope past the ingest\nlimit and drop every unrelated span sent with it. Size is measured in **UTF-8\nbytes**, so a body of non-ASCII text counts what it will actually cost.\n\nNothing in `extra` is scrubbed. Use `beforeSend` for redaction, and think twice\nbefore attaching a body that can carry tokens or personal data.\n\n```ts\nconst started = performance.now();\nconst res = await fetch('/api/orders');\nSauron.trackTransaction({\n  name: 'GET /api/orders',\n  op: 'http',\n  durationMs: performance.now() - started,\n  status: res.ok ? 'ok' : 'error',\n  httpMethod: 'GET',\n  httpStatus: res.status,\n  url: '/api/orders',\n});\n```\n\n#### Example: a `fetch` wrapper that records both bodies\n\nDrop-in replacement for `fetch` on the calls you care about. Note the\n`res.clone()` — reading the body consumes the stream, so the caller would get an\nempty response otherwise.\n\n```ts\nimport * as Sauron from '@edraj/sauron-browser';\n\nexport async function tracedFetch(\n  input: string,\n  init: RequestInit = {},\n): Promise<Response> {\n  const method = (init.method ?? 'GET').toUpperCase();\n  const path = new URL(input, location.origin).pathname;\n  const started = performance.now();\n\n  try {\n    const res = await fetch(input, init);\n    // Clone BEFORE reading: a Response body is a one-shot stream, and\n    // consuming it here would hand the caller an empty one.\n    const responseBody = await res.clone().text();\n\n    Sauron.trackTransaction({\n      name: `${method} ${path}`,          // grouping key — keep it low cardinality\n      op: 'http',\n      durationMs: performance.now() - started,\n      httpMethod: method,\n      httpStatus: res.status,\n      url: input,\n      status: res.ok ? 'ok' : 'error',\n      tags: { api: path.split('/')[2] ?? 'root' },\n      extra: {\n        request: typeof init.body === 'string' ? init.body : undefined,\n        response: responseBody,\n        response_bytes: responseBody.length,\n      },\n    });\n    return res;\n  } catch (err) {\n    Sauron.trackTransaction({\n      name: `${method} ${path}`,\n      op: 'http',\n      durationMs: performance.now() - started,\n      httpMethod: method,\n      url: input,\n      status: 'error',\n      extra: { request: init.body, error: String(err) },\n    });\n    throw err;\n  }\n}\n```\n\nOn the dashboard: **Transactions → the row → expand**. Both bodies render as a\nJSON tree, and every one of these finds it:\n\n```text\nextra.response:~9001        # substring, inside the stored response body\n@tag.api:orders             # indexed tag\nop:http http.status:>=500   # the failures\nduration:>2s                # the slow ones\n```\n\n#### Example: a client-side SQL query (`sql.js` / `wa-sqlite`)\n\nIf your app runs SQLite in the browser, spans work the same way. Put the\n**statement** in `extra` and keep `name` a stable label — a query with literals\nbaked in would mint a new dashboard row per execution.\n\n```ts\nfunction tracedQuery(db: Database, sql: string, params: unknown[] = []) {\n  const started = performance.now();\n  try {\n    const rows = db.exec(sql, params);\n    Sauron.trackTransaction({\n      // The LABEL, not the statement. `op` accepts only\n      // navigation|http|resource|screen_load|custom — anything else, `'db'`\n      // included, is coerced to 'custom', so pass 'custom' and say it with a tag.\n      name: 'SELECT orders',\n      op: 'custom',\n      durationMs: performance.now() - started,\n      status: 'ok',\n      tags: { db: 'sqlite', table: 'orders' },\n      extra: {\n        statement: sql,\n        row_count: rows[0]?.values.length ?? 0,\n        // Bind PARAMETERS are user data. Log them only if you have decided\n        // that is acceptable, or log their shape instead.\n        params,\n      },\n    });\n    return rows;\n  } catch (err) {\n    Sauron.trackTransaction({\n      name: 'SELECT orders',\n      op: 'custom',\n      durationMs: performance.now() - started,\n      status: 'error',\n      tags: { db: 'sqlite', table: 'orders' },\n      extra: { statement: sql, error: String(err) },\n    });\n    throw err;\n  }\n}\n```\n\nThen `@tag.table:orders duration:>500ms` is your slow-query list.\n\n### `setScreen(name)`\n\n```ts\nfunction setScreen(name: string): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | — (required) | The new screen/route name. |\n\nSets the current screen. On an actual change it also emits a `$screen` event\nwith `properties: { screen: name }` so dwell time can be computed server-side;\ncalling it again with the same name is a no-op. The current screen is stamped on\nevery subsequent event and error item. Returns `void`.\n\n```ts\nrouter.afterEach((to) => Sauron.setScreen(to.path));\n```\n\n### `getScreen()`\n\n```ts\nfunction getScreen(): string | null\n```\n\nReturns the current screen name, or `null` when none was ever set (and after\n`close()`, which resets it).\n\n```ts\nif (Sauron.getScreen() !== '/checkout') Sauron.setScreen('/checkout');\n```\n\n### `startWorkflow(name, options?)`\n\n```ts\nfunction startWorkflow(name: string, options?: { force?: boolean }): WorkflowResult\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | — (required) | Workflow name. Trimmed; rejected if empty after trimming or longer than 120 characters. |\n| `options.force` | `boolean` | `false` | Replace an already-active workflow instead of rejecting the call. |\n\nStarts a named, explicitly-bounded span of activity — e.g. `checkout`,\n`onboarding` — and mints a fresh **client-generated UUID** as its\n`workflowId`/wire `workflow_id`. While a workflow is active, every subsequent\n`track`, `captureException`, `captureMessage` and `trackTransaction` call is\nadditionally stamped with `workflow_id` + `workflow_name`, alongside whatever\nelse it already carries. `startWorkflow` itself emits a reserved\n`$workflow_start` event, stamped with the *new* workflow.\n\nWorkflows are entirely optional: an app that never calls `startWorkflow`\nbehaves exactly as before — no `workflow_id`/`workflow_name` fields are ever\nadded to any item.\n\nReturns a `WorkflowResult`:\n\n| `status` | Meaning |\n| --- | --- |\n| `'ok'` | Started (or replaced, with `force`). `workflowId` is the new id. |\n| `'already_active'` | Another workflow is already active and `force` was not set. Nothing changed. |\n| `'invalid_name'` | `name` was empty after trimming, or over 120 characters. Nothing changed. |\n| `'disabled'` | Called before `init()`, after the client was closed/disabled, or after the transport auto-disabled itself on a `401`/`403` — also returned if an unexpected internal error occurred. Nothing changed. |\n\nWith `force: true`, the previously-active workflow is closed first — emitting\n`$workflow_cancel` for it with `reason: 'superseded'` — and then the new one\nstarts. Without `force`, an active workflow simply blocks the call (logged as\na warning in `debug` mode). Telemetry never throws: every precondition failure\nreturns a status instead.\n\n`'disabled'` always means *nothing changed*, so it is never worth retrying\nblindly. If the workflow started but its `$workflow_start` event could not be\ndelivered, you still get `'ok'` and a `workflowId` — the workflow is live and\nstamping is active, and the server materializes the workflow from the first\nstamped event it receives regardless.\n\n```ts\nconst result = Sauron.startWorkflow('checkout');\nif (result.status === 'ok') {\n  console.log('workflow id', result.workflowId);\n}\n\n// Force-replace whatever workflow (if any) is currently active:\nSauron.startWorkflow('checkout', { force: true });\n```\n\n### `endWorkflow(name?)`\n\n```ts\nfunction endWorkflow(name?: string): WorkflowResult\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | current workflow | If given, must match the active workflow's name or the call is rejected. |\n\nEnds the active workflow: emits `$workflow_end` carrying `duration_ms` (the\ntime since `startWorkflow`), then clears the active workflow.\n\n| `status` | Meaning |\n| --- | --- |\n| `'ok'` | Ended. `workflowId` is the id that was closed. |\n| `'not_active'` | No workflow is active. Nothing changed. |\n| `'name_mismatch'` | `name` was given but does not match the active workflow. Nothing changed. |\n| `'disabled'` | Called before `init()`, after the client was closed/disabled, or after the transport auto-disabled itself on a `401`/`403` — also returned if an unexpected internal error occurred. Nothing changed. |\n\nA `name` that is itself malformed — empty, whitespace-only, or over 120\ncharacters — reports `'name_mismatch'`, not `'invalid_name'`: it cannot match\nthe active workflow, and the call named a workflow that is not the active one.\n`'invalid_name'` is reserved for `startWorkflow`, where the name is the thing\nbeing created rather than a guard on which workflow to close.\n\n`'ok'` always means the workflow really is closed locally, even in the rare\ncase where the `$workflow_end` event itself could not be delivered — so it is\nnever correct to see `'ok'` and still have `getWorkflow()` return non-null.\n\n```ts\nSauron.startWorkflow('checkout');\n// ... later\nSauron.endWorkflow(); // { status: 'ok', workflowId: '...' }\n```\n\n### `cancelWorkflow(name?, options?)`\n\n```ts\nfunction cancelWorkflow(name?: string, options?: { reason?: string }): WorkflowResult\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | current workflow | If given, must match the active workflow's name or the call is rejected. |\n| `options.reason` | `string` | `'user'` | Free-form cancellation reason. Trimmed and capped at 120 characters. |\n\nCancels the active workflow: emits `$workflow_cancel` carrying `duration_ms`\nand `reason`, then clears the active workflow. Same status values and\npreconditions as `endWorkflow` (`'ok'` / `'not_active'` / `'name_mismatch'` /\n`'disabled'`), including the rule that a malformed `name` reports\n`'name_mismatch'`. `startWorkflow(..., { force: true })` uses this internally\nwith `reason: 'superseded'` when it replaces an active workflow.\n\n```ts\nSauron.cancelWorkflow(); // reason defaults to 'user'\nSauron.cancelWorkflow('checkout', { reason: 'payment declined' });\n```\n\n### `getWorkflow()`\n\n```ts\nfunction getWorkflow(): ActiveWorkflow | null\n```\n\nReturns the active workflow — `{ workflowId, name, startedAt }` — or `null`\nwhen none is active (including before `init()`, and after `close()`, which\nresets it).\n\nA workflow with no stamped activity for 30 minutes is surfaced as `abandoned`\nwhen queried on the dashboard/API. That status is derived on read from the\nlast stamped event's timestamp — it is never stored, so there is nothing for\nthe client to do; an \"abandoned\" workflow that later receives another stamped\nevent simply reads as active again.\n\n```ts\nconst active = Sauron.getWorkflow();\nif (active) {\n  console.log(`${active.name} running for`, Date.now() - Date.parse(active.startedAt), 'ms');\n}\n```\n\n### `addBreadcrumb(breadcrumb, hint?)`\n\n```ts\nfunction addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: Hint): void\n```\n\n| Field of `breadcrumb` | Type | Default | Description |\n| --- | --- | --- | --- |\n| `type` | `string` | `'default'` | Coarse kind, e.g. `'navigation'`. |\n| `category` | `string` | `'default'` | Fine kind, e.g. `'ui.click'`, `'fetch'`. |\n| `message` | `string \\| null` | `null` | Short description. |\n| `level` | `Level` | `'info'` | Severity. |\n| `timestamp` | `string` | now, ISO-8601 UTC | Overrides the recorded time. |\n| `data` | `Record<string, unknown> \\| null` | `null` | Structured payload. |\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `hint` | `Hint` | `undefined` | Forwarded to `beforeBreadcrumb` only. |\n\nThe breadcrumb runs through `beforeBreadcrumb` and lands in the ring buffer.\nBreadcrumbs are never sent on their own — the trail is copied onto every error\nitem. Returns `void`.\n\n```ts\nSauron.addBreadcrumb({\n  type: 'default',\n  category: 'auth',\n  level: 'info',\n  message: 'token refreshed',\n  data: { expires_in: 3600 },\n});\n```\n\n### `setUser(user)`\n\n```ts\nfunction setUser(user: UserInput): void\n```\n\n| Field of `user` | Type | Default | Description |\n| --- | --- | --- | --- |\n| `id` | `string \\| null` | `null` | User id; also becomes the `distinct_id` for later events. |\n| `email` | `string \\| null` | `null` | User email. |\n| `traits` | `Record<string, unknown>` | `{}` | Arbitrary user traits. |\n\nPass `null` to clear the user entirely. The user is written to `context.user` on\nevery envelope, and onto `item.user` of error items while one is set. This is a\nreplace, not a merge. Returns `void`.\n\n```ts\nSauron.setUser({ id: 'u_123', email: 'ada@example.com', traits: { plan: 'pro' } });\nSauron.setUser(null); // on logout\n```\n\n### `setTag(key, value)`\n\n```ts\nfunction setTag(key: string, value: string): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `key` | `string` | — (required) | Tag key. |\n| `value` | `string` | — (required) | Tag value (strings only — tags are indexed). |\n\nSets one tag on the global scope; it is lifted onto every later error and event\nitem. Returns `void`.\n\n```ts\nSauron.setTag('tenant', 'acme');\n```\n\n### `setTags(tags)`\n\n```ts\nfunction setTags(tags: Record<string, string>): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `tags` | `Record<string, string>` | — (required) | Batch of tags. |\n\nShallow-merges the batch into the scope, last-write-wins per key. Keys not\npresent are left alone. Returns `void`.\n\n```ts\nSauron.setTags({ tenant: 'acme', tier: 'enterprise' });\n```\n\n### `setContext(name, block)`\n\n```ts\nfunction setContext(name: string, block: Record<string, unknown>): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `name` | `string` | — (required) | Block name, e.g. `'order'`. |\n| `block` | `Record<string, unknown>` | — (required) | The block's contents. |\n\nReplaces the whole named block (no deep merge). Dev-owned contexts are distinct\nfrom the machine-detected `context` on the envelope and never overwrite it.\nReturns `void`.\n\n```ts\nSauron.setContext('order', { id: 7, total: 42.5 });\n```\n\n### `setExtra(key, value)`\n\n```ts\nfunction setExtra(key: string, value: unknown): void\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `key` | `string` | — (required) | Key in the freeform bag. |\n| `value` | `unknown` | — (required) | Any JSON-serializable value. |\n\nSets one freeform value on the scope. Returns `void`.\n\n```ts\nSauron.setExtra('feature_flags', ['new-cart', 'fast-checkout']);\n```\n\n### `flush(timeoutMs?)`\n\n```ts\nfunction flush(timeoutMs?: number): Promise<boolean>\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `timeoutMs` | `number` | `undefined` (wait indefinitely) | Give up after this many milliseconds. |\n\nDrains the offline queue, then posts everything buffered in `maxBatch`-sized\nenvelopes. Resolves `true` on completion, `false` if `timeoutMs` elapsed first\nor if `init()` was never called. Resolves `true` immediately when the client has\nbeen disabled by a 401/403.\n\n```ts\nawait Sauron.flush(2000);\n```\n\n### `close(timeoutMs?)`\n\n```ts\nfunction close(timeoutMs?: number): Promise<boolean>\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `timeoutMs` | `number` | `undefined` (wait indefinitely) | Passed straight to the inner flush. |\n\nFlushes, then tears the SDK down: stops the flush timer and the `online`\nlistener, removes the unload listeners, clears the navigation hook and the\ncurrent screen, and restores every patched global in reverse order. Resolves to\nthe flush result. The client stays registered but disabled — call `init()` again\nto restart.\n\n```ts\nawait Sauron.close(2000);\n```\n\n### `getClient()`\n\n```ts\nfunction getClient(): SauronClient | null\n```\n\nReturns the active client, or `null` before `init()`.\n\n```ts\nconst enabled = Sauron.getClient()?.isEnabled() ?? false;\n```\n\n### `SauronClient`\n\nThe client class, exported for typing and for the escape hatches below. You get\nan instance from `init()` or `getClient()` — do not construct it yourself.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `options` | `readonly ResolvedOptions` | Fully-resolved options with defaults applied. |\n| `dsn` | `readonly Dsn` | The parsed DSN. |\n| `install()` | `(): void` | Install integrations + start the transport. Called by `init()`; a second call is a no-op. |\n| `getScope()` | `(): Scope` | The mutable scope (user, breadcrumbs, tags, contexts, extra). |\n| `isEnabled()` | `(): boolean` | `false` once the client has been explicitly `disable()`d/`teardown()`'d/`close()`d, **or** the transport has auto-disabled itself on a `401`/`403`. Computed from the transport's own state on every call, so a mid-session `401`/`403` flips this to `false` immediately — without the app ever calling `disable()`/`close()`. |\n| `getDistinctId()` | `(): string \\| null` | User id when identified, else the anonymous id (minting one if needed). |\n| `getAnonymousId()` | `(): string \\| null` | The anonymous id, or `null` if one was never needed. |\n| `makeEnvelope(items)` | `(items: EnvelopeItem[]): Envelope` | Stamp a fresh envelope (new `sent_at`, current context) around `items`. |\n| `addBreadcrumb(crumb, hint?)` | `(Breadcrumb, Hint?): void` | Full-shape breadcrumb, runs `beforeBreadcrumb`. |\n| `captureItem(item, hint?)` | `(EnvelopeItem, Hint?): void` | Sampling + enrichment + workflow stamping + `beforeSend` + enqueue. |\n| `flush(timeoutMs?)` | `(number?): Promise<boolean>` | Same as the module-level `flush`. |\n| `disable()` | `(): void` | Stop accepting and sending; drops pending work. |\n| `teardown()` | `(): void` | Restore globals and stop timers/listeners without flushing. |\n| `close(timeoutMs?)` | `(number?): Promise<boolean>` | Flush, then `teardown()`. |\n\n> **Workflow stamping happens inside `captureItem`.** That is why `track`,\n> `captureException`, `captureMessage` and `trackTransaction` all pick up the active\n> workflow automatically. If you hand-build an item and pass it to `captureItem` yourself,\n> your own `workflow_id` / `workflow_name` win — the SDK will not overwrite them. Set\n> **both or neither**: the server treats them as a pair and silently drops the attribution\n> if only one is present, so the SDK logs a warning in that case. `identify` and\n> breadcrumb-batch items are never stamped — the server has no workflow columns for them.\n\n```ts\nimport { getClient } from '@edraj/sauron-browser';\n\nconst trail = getClient()?.getScope().getBreadcrumbs() ?? [];\n```\n\n### `parseDsn(dsn)` and `DsnError`\n\n```ts\nfunction parseDsn(dsn: string): Dsn\nclass DsnError extends Error\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `dsn` | `string` | — (required) | `https://<public_key>@<host>/<environment_id>`. |\n\nReturns a `Dsn` with `raw`, `publicKey`, `host` (`host:port`), `hostname`,\n`protocol` (`http` or `https`, no colon), `projectId` (the DSN's path\nsegment — despite the name, this is the **environment** id since the ingest\nkey now lives on the environment, not the app), `envelopeUrl`\n(`<protocol>://<host>/api/<environment_id>/envelope`) and `beaconUrl` (the same\nURL with `?k=<public_key>`).\n\nThrows `DsnError` (message prefixed `[sauron] invalid DSN:`) for an empty or\nnon-string value, an unparseable URL, a protocol other than `http`/`https`, a\nmissing public key, a DSN that carries a password component, a missing host, or\na missing environment-id path segment.\n\n```ts\nimport { parseDsn, DsnError } from '@edraj/sauron-browser';\n\ntry {\n  const dsn = parseDsn('https://pk_test@ingest.example.com/42');\n  console.log(dsn.envelopeUrl); // https://ingest.example.com/api/42/envelope\n} catch (err) {\n  if (err instanceof DsnError) console.error(err.message);\n}\n```\n\n### `buildEnvelope(header, context, items)`\n\n```ts\nfunction buildEnvelope(\n  header: EnvelopeHeader,\n  context: Context,\n  items: EnvelopeItem[],\n): Envelope\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `header` | `EnvelopeHeader` | — (required) | `dsn`, `sdk`, `sent_at`, `release`. |\n| `context` | `Context` | — (required) | `device`, `os`, `app`, `runtime`, `user`. |\n| `items` | `EnvelopeItem[]` | — (required) | The payload items. |\n\nA pure constructor for the canonical envelope shape (`header`, `context`,\n`items`, in that order). Useful for tests and for hand-rolled delivery.\n\n```ts\nimport { buildEnvelope, SDK_NAME, SDK_VERSION } from '@edraj/sauron-browser';\n\nconst envelope = buildEnvelope(\n  {\n    dsn: 'https://pk_test@localhost:8081/1',\n    sdk: { name: SDK_NAME, version: SDK_VERSION },\n    sent_at: new Date().toISOString(),\n    release: null,\n  },\n  context,\n  [item],\n);\n```\n\n### `parseStackString(stack)`, `parseError(err)`, `isInAppFrame(filename)`\n\n```ts\nfunction parseStackString(stack: string | undefined | null): Frame[]\nfunction parseError(err: unknown): Frame[]\nfunction isInAppFrame(filename: string | null): boolean\n```\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `stack` | `string \\| undefined \\| null` | — (required) | A raw `Error.stack` string. `null`/`undefined` yields `[]`. |\n| `err` | `unknown` | — (required) | Any value; its string `.stack` is parsed, else `[]`. |\n| `filename` | `string \\| null` | — (required) | A frame filename. |\n\n`parseStackString` handles both the V8/Chrome/Node/Edge (`at fn (file:line:col)`)\nand the Firefox/Safari (`fn@file:line:col`) formats, skips non-frame lines,\ncaps at 50 frames keeping the ones nearest the crash, and returns them with the\n**crashing frame last**. No symbolication happens client-side.\n\n`isInAppFrame` returns `true` for bare/relative paths and same-origin absolute\nURLs, and `false` for cross-origin URLs, `<anonymous>`, `node:*` and\n`internal/*`.\n\n```ts\nimport { parseError, isInAppFrame } from '@edraj/sauron-browser';\n\nconst frames = parseError(new Error('boom'));\nconst appFrames = frames.filter((f) => isInAppFrame(f.filename));\n```\n\n### `SDK_NAME`, `SDK_VERSION`\n\n```ts\nconst SDK_NAME: string  // 'sauron.javascript'\nconst SDK_VERSION: string // '1.7.0'\n```\n\nThe SDK identity embedded in `header.sdk` of every envelope.\n\n### Exported types\n\nAll wire-contract and option types are exported for your own typing:\n\n- Enums / unions: `Level`, `ItemType`, `TransactionOp`, `WorkflowStatus`.\n- Item shapes: `Frame`, `Mechanism`, `ExceptionValue`, `Breadcrumb`, `ErrorItem`,\n  `EventItem`, `IdentifyItem`, `BreadcrumbBatchItem`, `TransactionItem`,\n  `EnvelopeItem`.\n- Envelope shapes: `DeviceContext`, `OsContext`, `AppContext`, `RuntimeContext`,\n  `UserContext`, `Context`, `SdkInfo`, `EnvelopeHeader`, `Envelope`.\n- Input / option shapes: `Hint`, `UserInput`, `BeforeSend`, `BeforeBreadcrumb`,\n  `TransportOptions`, `InitOptions`, `CaptureOptions`, `TrackOptions`,\n  `ResolvedOptions`, `BreadcrumbInput`, `TransactionInput`, `Dsn`,\n  `WorkflowResult`, `ActiveWorkflow`.\n\n```ts\nimport type { EnvelopeItem, Hint, InitOptions } from '@edraj/sauron-browser';\n\nconst beforeSend = (item: EnvelopeItem, hint?: Hint): EnvelopeItem | null =>\n  item.type === 'error' ? item : null;\nconst options: InitOptions = { dsn: '...', release: 'web@1.4.2', beforeSend };\n```\n\n## Automatic instrumentation\n\n`init()` patches the following globals. Every patch chains or defers to the\noriginal — the app's own handlers, console output and `fetch` results are never\nswallowed — and every one is restored by `close()`.\n\nOn by default:\n\n| Global | What it records |\n| --- | --- |\n| `window.onerror` | Error item, mechanism `{ type: 'onerror', handled: false }`, level `error`. The previous handler is still called with its original arguments. |\n| `window.onunhandledrejection` | Error item from `event.reason`, mechanism `{ type: 'onunhandledrejection', handled: false }`, level `error`. |\n| `console.log/info/warn/error/debug` | Breadcrumb, category `console`, level mapped (`warn`→`warning`, `error`→`error`, `debug`→`debug`, else `info`), message = arguments joined and truncated to 512 chars, `data: { arguments: n }`. Output is untouched. |\n| `document` click listener (capture, passive) | Breadcrumb, category `ui.click`, message = a `tag#id.class` selector (up to 3 classes). Element text and attribute values are never serialized. |\n| `history.pushState` / `replaceState` / `popstate` | Breadcrumb, type `navigation`, category `history`, `data: { from, to, operation }` — paths plus the direction: `push`, `replace` or `pop`. Same-path transitions are skipped. |\n| `fetch` | Breadcrumb, category `fetch`, message `METHOD url`, `data: { method, url, status_code }`, level `warning` for status >= 400. |\n| `XMLHttpRequest.prototype.open` / `send` | Breadcrumb, category `xhr`, same shape as `fetch`. |\n| `document` `visibilitychange` + window `pagehide` | Beacon flush of the pending batch on unload. |\n| window `online` | Drains the offline queue. |\n\nOpt-in:\n\n| Option | What it adds |\n| --- | --- |\n| `performance: true` | A `navigation` transaction for the initial page load (Navigation Timing, captured on `load`), an `http` transaction per instrumented `fetch` (`name` = `METHOD /path`, `status` `ok`/`error`), and a `navigation` transaction per SPA route change measured over one animation frame. No-op when `document` is undefined. |\n| `screenTracking: true` | Sets the screen to the new path on each SPA History navigation, which emits a `$screen` event on change. |\n\n`operation` uses the same vocabulary as the Flutter SDK's\n`SauronNavigatorObserver` (`push` / `pop` / `replace` / `remove`), so a trail\nreads the same whichever SDK sent it. `remove` has no web equivalent and is\nnever emitted here. One limit worth knowing: **a forward navigation is recorded\nas `pop`.** `history.forward()` fires the same `popstate` event as\n`history.back()` and carries nothing to separate them, so `pop` means \"moved\nthrough history\", not specifically \"went back\". Distinguishing them would mean\nwriting to `history.state`, which the host app's router also owns — not a\ntrade the SDK makes.\n\nTwo guards keep the SDK from observing itself: a reentrancy flag held while SDK\ncode runs, and a denylist on the DSN host. Requests the transport makes are\ntherefore never turned into breadcrumbs or transactions. Wrappers are tagged, so\na double `init()` never stacks two layers on the same global.\n\nIntegrations that need an absent global (no `document`, no `history`, no\n`XMLHttpRequest`, no writable `localStorage`) simply skip installation, so\nimporting and initializing during SSR does not throw.\n\n## Scope & metadata\n\nThere is a single global scope per client, holding the user, the breadcrumb ring\nbuffer, `tags`, `contexts` and `extra`.\n\nPrecedence for `tags` / `contexts` / `extra`, lowest to highest:\n\n1. **`init` defaults** — `tags`, `contexts`, `extra` are seeded into the scope\n   when the client is constructed.\n2. **Scope setters** — `setTag`, `setTags`, `setContext`, `setExtra` write into\n   that same store, so they overwrite the init defaults for the keys they touch\n   (last write wins) and leave the rest alone.\n3. **Per-call overrides** — `hint.tags` / `hint.contexts` / `hint.extra` on\n   `captureException` and `captureMessage`, and `options.tags` /\n   `options.contexts` / `options.extra` on `track`. These win for that one item\n   and never mutate the scope.\n\nThe merge is shallow, per top-level key: a per-call tag replaces the scope tag\nof the same key; a per-call **context block replaces the whole same-named scope\nblock** (no deep merge); other blocks and keys are preserved. When the merged\nresult is empty the field is omitted from the wire item entirely — the backend\ndefaults it to `{}`.\n\nOther scope data:\n\n- **user** — `setUser()` replaces it wholesale; `identify()` also replaces it\n  with `{ id, traits }`. It is written to `context.user` on every envelope, and\n  additionally onto `item.user` of error items while a user is set.\n- **breadcrumbs** — capped at `maxBreadcrumbs`, FIFO eviction. The whole trail\n  is copied onto every error item; it is never sent on its own.\n- **screen** — seeded by `init({ screen })`, changed by `setScreen()` (or\n  `screenTracking`). Stamped on every event and error item.\n  `TrackOptions.screen` overrides it for one event, `hint.screen` for one\n  `captureException`.\n- **identity** — `device_id` persists in `localStorage` under\n  `sauron.device_id`; `session_id` persists in `sessionStorage` under\n  `sauron.session_id`; `identify()` additionally persists a short one-way\n  digest (never the id itself) of the last identified user in `localStorage`\n  under `sauron.last_identified`, used to detect a login by a different\n  person on a device where `reset()` was never wired — see \"Reset on logout\"\n  in the wiki. This is not a security boundary (an unkeyed hash over a\n  possibly low-entropy id, e.g. an email, is a confirmation oracle, not a\n  secret) — it exists only so the key isn't a second plaintext copy of the\n  app's user id. All fall back to a per-process in-memory id when Web Storage\n  is unavailable.\n\n```ts\nSauron.init({ dsn, release: 'web@1.4.2', tags: { tier: 'free' }, extra: { build: 'ci-42' } });\nSauron.setTag('tier', 'pro');                       // scope beats init default\nSauron.track('upgraded', {}, { tags: { tier: 'trial' } });\n// -> event tags: { tier: 'trial' }, extra: { build: 'ci-42' }\n```\n\n## Bundlers & CDN\n\n- `\"type\": \"module\"` with a dual build: `import` resolves `dist/index.js`\n  (ESM), `require` resolves `dist/index.cjs`, types come from\n  `dist/index.d.ts`. `package.json` itself is exported as `./package.json`;\n  nothing else is deep-importable.\n- `\"sideEffects\": false` — bundlers may drop unused exports. Importing the\n  package does nothing on its own; instrumentation is installed by `init()`.\n- Built with tsup, target `es2020`, with source maps and generated declarations.\n- `dist/sauron.min.js` and `dist/sauron.es5.min.js` are self-contained IIFE\n  builds that define `window.Sauron` — see\n  [Script tag & Google Tag Manager](#script-tag--google-tag-manager). They are\n  built by `scripts/build-global.mjs` (esbuild, plus SWC for the ES5 file), not\n  tsup, and are not reachable through `exports`: load them by URL.\n- A module script against an ESM-serving CDN also works:\n\n```html\n<script type=\"module\">\n  import { Sauron } from 'https://esm.sh/@edraj/sauron-browser@1.8.0';\n  Sauron.init({ dsn: 'https://pk_test@ingest.example.com/42', release: 'web@1.4.2' });\n</script>\n```\n\n- The only runtime dependency is `fflate`, imported dynamically and only when\n  the platform lacks `CompressionStream`. Bundlers will emit it as a separate\n  async chunk. The script-tag builds inline only its `gzipSync`.\n- Initialize as early as possible — errors thrown before `init()` are not\n  captured.\n\n## Transport & delivery\n\n**Batching.** Items are buffered in memory and flushed every `flushIntervalMs`\n(default 5000 ms), immediately once `maxBatch` items are pending (default 30,\nclamped to `[1, 1000]`), and on demand via `flush()`/`close()`. Each `flush()`\ndrains the offline queue first, then posts the buffered items in\n`maxBatch`-sized envelopes.\n\n**Request.** `POST <protocol>://<host>/api/<environment_id>/envelope` with:\n\n```\nContent-Type: application/json\nX-Sauron-Key: <public_key>\nContent-Encoding: gzip        # only when the body was compressed\n```\n\nThe body is the canonical envelope — `header` + `context` + `items[]` —\nidentical across the JavaScript, Node, Python, Flutter and C# SDKs.\n\n**Compression.** Payloads of 1024 bytes or more are gzipped with the native\n`CompressionStream('gzip')` when available, falling back to a lazily imported\n`fflate`. If neither works the envelope is sent uncompressed rather than\ndropped. Smaller payloads are sent as plain JSON with no `Content-Encoding`.\n\n**HTTP client.** The native `fetch` captured *before* the integrations wrap it,\nso ingest traffic never instruments itself; `XMLHttpRequest` is the fallback\nwhen `fetch` is absent. `keepalive: true` is set for bodies up to 64 KiB.\n\n**Response handling.**\n\n| Status | Action |\n| --- | --- |\n| `200`, `202` | Success — drop the batch. |\n| `400` | Non-retryable — drop the batch. |\n| `401`, `403` | Disable the client permanently: pending work is dropped and nothing further is sent until the next `init()`. |\n| `408` | Retry with backoff. |\n| `413` | Split the batch in half and retry each half. A single item that is still too large is parked in the offline queue. |\n| `429` | Wait `Retry-After` (seconds or HTTP-date, clamped to 30 s; 1000 ms if unparseable), then retry. |\n| `5xx` | Retry with backoff. |\n| other `4xx` | Drop the batch. |\n| network error / throw | Retry with backoff. |\n\nBackoff is full-jitter: a uniform random delay in\n`[0, min(30_000, 1000 * 2^attempt)]` ms. After 5 retries the serialized envelope\nis parked in the offline queue.\n\n**Offline queue.** A FIFO list under the `localStorage` key `sauron:queue:v1`,\nbyte-capped at `maxQueueBytes` (default 1 MiB); the oldest entries are evicted\nfirst and at least one entry is always kept. It is drained at `init()`, at the\nstart of every `flush()`, and on the window `online` event. If a drained\nenvelope still fails, **it and every envelope behind it are re-parked at the head\nof the queue** (order preserved) and draining stops to avoid a tight loop — a\nsingle 500 on reconnect costs you nothing. Same on a 401/403: the client\ndisables itself but the backlog is kept, so fixing the key and re-`init()`ing\nstill delivers it. When `localStorage` is unavailable the queue is disabled and\nfailed envelopes are dropped.\n\n> Through 1.3.0 the drain deleted the whole `localStorage` backlog up front and\n> re-parked only the one envelope that failed, so everything queued behind it was\n> lost — the exact reconnect-then-one-500 scenario the queue exists for.\n\n**Page unload.** On `visibilitychange` → `hidden` and on `pagehide`, the pending\nbatch is chunked to 1000 items and handed to `navigator.sendBeacon` as an\nuncompressed `application/json` Blob posted to\n`POST /api/<environment_id>/envelope?k=<public_key>` (the key moves to the query\nstring because beacons cannot set headers). Chunks larger than 64 KiB, or a\n`sendBeacon` that is unavailable or refuses, are parked in the offline queue for\nthe next page load.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n| --- | --- | --- |\n| Nothing arrives, no client-side errors | The gateway is not exposed at `/api/{environment_id}/envelope` on the host root. A DSN cannot express a path prefix, so the SDK posts to the root path and a proxy that serves ingest under a sub-path silently 404s. | Expose ingest at `/api/{environment_id}/envelope` on the DSN host root. |\n| Nothing arrives | `init()` was never called, or was called after the failing code ran. | Call `init()` first, as early in the page as possible. |\n| `[sauron] client disabled` in the console, or `isEnabled()` unexpectedly `false` mid-session | The gateway returned 401/403 — wrong, revoked or foreign-project public key. `isEnabled()` flips to `false` automatically; nothing else changes. | Fix the DSN key/project; re-`init()` after correcting. |\n| `DsnError` thrown at `init()` | Malformed DSN: bad protocol, missing public key, a password component, or a missing environment-id path segment. | Use `https://<public_key>@<host>/<environment_id>`. |\n| `init()` throws `` [sauron] init() requires a `release` `` | `release` is missing, not a string, or blank. It is required as of v1.7.0, so code written against 1.6 or earlier throws here until it passes one. | Pass the app version this build reports as: `init({ dsn, release: 'web@1.4.2' })`. |\n| Only some errors show up | `sampleRate` below 1 (errors and messages are sampled; events, identifies and transactions are not). | Set `sampleRate: 1`. |\n| Errors arrive with no breadcrumbs | `maxBreadcrumbs: 0`, or `beforeBreadcrumb` returned `null`. | Raise `maxBreadcrumbs`; check the hook. |\n| Items disappear silently | `beforeSend` returned `null`, or it threw (the original is then sent and a warning logged). | Enable `debug: true` and read the `[sauron]` logs. |\n| Events lost when a tab closes after a busy session | The unload beacon chunk exceeded 64 KiB, or `sendBeacon` is unavailable. | Nothing to do — the payload is parked in `localStorage` and posted on the next page load. |\n| Nothing persists in private mode | `localStorage`/`sessionStorage` are blocked, so the offline queue is disabled and ids fall back to in-memory. | Expected; reduce `flushIntervalMs` to shorten the loss window. |\n| No transactions | `performance` defaults to `false`. | `init({ performance: true })`, or call `trackTransaction()` manually. |\n| Screen is always `null` | Neither `init({ screen })`, `setScreen()` nor `screenTracking: true` was used. | Set one of them. |\n| No debug output | `debug` defaults to `false`. | `init({ debug: true })`; logs are prefixed `[sauron]`. |\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck    # tsc --noEmit\nnpm test             # vitest run\nnpm run test:watch   # vitest\nnpm run build        # tsup -> dist/ (esm + cjs + d.ts + sourcemaps),\n                     # then scripts/build-global.mjs -> the two script-tag files\nnpm run dev          # tsup --watch\n```\n\n`npm run prepublishOnly` chains typecheck, tests and build.\n\n## License\n\nLGPL-3.0-only — GNU Lesser General Public License v3.0. LGPLv3 applies on top of\nthe GNU GPL v3, whose text ships alongside it in `COPYING`.\n\nRepo: <https://github.com/edraj/sauron> — wiki:\n<https://github.com/edraj/sauron/wiki>\n","readmeFilename":"README.md"}