diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb608200..934728c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,8 +167,20 @@ jobs: with: path: node_modules key: node-modules-${{ matrix.os }}-${{ hashFiles('bun.lock', 'patches/**') }} - - if: steps.cache.outputs.cache-hit != 'true' - run: bun install --frozen-lockfile + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + # Retry logic for Windows Bun patch bug (ENOTEMPTY errors) + for i in 1 2 3; do + if bun install --frozen-lockfile; then + exit 0 + fi + echo "Attempt $i failed, clearing Bun cache and retrying..." + bun pm cache rm 2>/dev/null || true + done + echo "All install attempts failed" + exit 1 - name: Build env: SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }} diff --git a/bun.lock b/bun.lock index 4437b49b..1fa6aea4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,8 +6,9 @@ "name": "sentry", "devDependencies": { "@biomejs/biome": "2.3.8", + "@sentry/bun": "10.38.0", "@sentry/esbuild-plugin": "^2.23.0", - "@sentry/node": "^10.36.0", + "@sentry/node": "10.38.0", "@stricli/auto-complete": "^1.2.4", "@stricli/core": "^1.2.4", "@types/bun": "latest", @@ -20,13 +21,14 @@ "tinyglobby": "^0.2.15", "typescript": "^5", "ultracite": "6.3.10", + "uuidv7": "^1.1.0", "zod": "^3.24.0", }, }, }, "patchedDependencies": { - "@sentry/core@10.36.0": "patches/@sentry%2Fcore@10.36.0.patch", "@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch", + "@sentry/core@10.38.0": "patches/@sentry%2Fcore@10.38.0.patch", }, "packages": { "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="], @@ -155,57 +157,57 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.210.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw=="], "@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.210.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.210.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g=="], + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="], - "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.57.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-hgHnbcopDXju7164mwZu7+6mLT/+O+6MsyedekrXL+HQAYenMqeG7cmUOE0vI6s/9nW08EGHXpD+Q9GhLU1smA=="], + "@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.58.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fjpQtH18J6GxzUZ+cwNhWUpb71u+DzT7rFkg5pLssDGaEber91Y2WNGdpVpwGivfEluMlNMZumzjEqfg8DeKXQ=="], - "@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.53.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-SoFqipWLUEYVIxvz0VYX9uWLJhatJG4cqXpRe1iophLofuEtqFUn8YaEezjz2eJK74eTUQ0f0dJVOq7yMXsJGQ=="], + "@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.54.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-43RmbhUhqt3uuPnc16cX6NsxEASEtn8z/cYV8Zpt6EP4p2h9s4FNuJ4Q9BbEQ2C0YlCCB/2crO1ruVz/hWt8fA=="], - "@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.27.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8e7n8edfTN28nJDpR/H59iW3RbW1fvpt0xatGTfSbL8JS4FLizfjPxO7JLbyWh9D3DSXxrTnvOvXpt6V5pnxJg=="], + "@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.28.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ExXGBp0sUj8yhm6Znhf9jmuOaGDsYfDES3gswZnKr4MCqoBWQdEFn6EoDdt5u+RdbxQER+t43FoUihEfTSqsjA=="], - "@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.58.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-UuGst6/1XPcswrIm5vmhuUwK/9qx9+fmNB+4xNk3lfpgQlnQxahy20xmlo3I+LIyA5ZA3CR2CDXslxAMqwminA=="], + "@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pMKV/qnHiW/Q6pmbKkxt0eIhuNEtvJ7sUAyee192HErlr+a1Jx+FZ3WjfmzhQL1geewyGEiPGkmjjAgNY8TgDA=="], - "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.29.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JXPygU1RbrHNc5kD+626v3baV5KamB4RD4I9m9nUTd/HyfLZQSA3Z2z3VOebB3ChJhRDERmQjLiWvwJMHecKPg=="], + "@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.30.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n3Cf8YhG7reaj5dncGlRIU7iT40bxPOjsBEA5Bc1a1g6e9Qvb+JFJ7SEiMlPbUw4PBmxE3h40ltE8LZ3zVt6OA=="], - "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.53.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h49axGXGlvWzyQ4exPyd0qG9EUa+JP+hYklFg6V+Gm4ZC2Zam1QeJno/TQ8+qrLvsVvaFnBjTdS53hALpR3h3Q=="], + "@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.54.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8dXMBzzmEdXfH/wjuRvcJnUFeWzZHUnExkmFJ2uPfa31wmpyBCMxO59yr8f/OXXgSogNgi/uPo9KW9H7LMIZ+g=="], - "@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-wjtSavcp9MsGcnA1hj8ArgsL3EkHIiTLGMwqVohs5pSnMGeao0t2mgAuMiv78KdoR3kO3DUjks8xPO5Q6uJekg=="], + "@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+yWVVY7fxOs3j2RixCbvue8vUuJ1inHxN2q1sduqDB0Wnkr4vOzVKRYl/Zy7B31/dcPS72D9lo/kltdOTBM3bQ=="], - "@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.56.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HgLxgO0G8V9y/6yW2pS3Fv5M3hz9WtWUAdbuszQDZ8vXDQSd1sI9FYHLdZW+td/8xCLApm8Li4QIeCkRSpHVTg=="], + "@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.57.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Os4THbvls8cTQTVA8ApLfZZztuuqGEeqog0XUnyRW7QVF0d/vOVBEcBCk1pazPFmllXGEdNbbat8e2fYIWdFbw=="], - "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.210.0", "", { "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/instrumentation": "0.210.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-dICO+0D0VBnrDOmDXOvpmaP0gvai6hNhJ5y6+HFutV0UoXc7pMgJlJY3O7AzT725cW/jP38ylmfHhQa7M0Nhww=="], + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/instrumentation": "0.211.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA=="], - "@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-2tEJFeoM465A0FwPB0+gNvdM/xPBRIqNtC4mW+mBKy+ZKF9CWa7rEqv87OODGrigkEDpkH8Bs1FKZYbuHKCQNQ=="], + "@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.59.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-875UxzBHWkW+P4Y45SoFM2AR8f8TzBMD8eO7QXGCyFSCUMP5s9vtt/BS8b/r2kqLyaRPK6mLbdnZznK3XzQWvw=="], - "@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.19.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-PMJePP4PVv+NSvWFuKADEVemsbNK8tnloHnrHOiRXMmBnyqcyOTmJyPy6eeJ0au90QyiGB2rzD8smmu2Y0CC7A=="], + "@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.20.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-yJXOuWZROzj7WmYCUiyT27tIfqBrVtl1/TwVbQyWPz7rL0r1Lu7kWjD0PiVeTCIL6CrIZ7M2s8eBxsTAOxbNvw=="], - "@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.54.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XYXKVUH+0/Ur29jMPnyxZj32MrZkWSXHhCteTkt/HzynKnvIASmaAJ6moMOgBSRoLuDJFqPew68AreRylIzhhg=="], + "@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.55.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FtTL5DUx5Ka/8VK6P1VwnlUXPa3nrb7REvm5ddLUIeXXq4tb9pKd+/ThB1xM/IjefkRSN3z8a5t7epYw1JLBJQ=="], - "@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.58.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-602W6hEFi3j2QrQQBKWuBUSlHyrwSCc1IXpmItC991i9+xJOsS4n4mEktEk/7N6pavBX35J9OVkhPDXjbFk/1A=="], + "@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.59.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-K9o2skADV20Skdu5tG2bogPKiSpXh4KxfLjz6FuqIVvDJNibwSdu5UvyyBzRVp1rQMV6UmoIk6d3PyPtJbaGSg=="], - "@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.54.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-LPji0Qwpye5e1TNAUkHt7oij2Lrtpn2DRTUr4CU69VzJA13aoa2uzP3NutnFoLDUjmuS6vi/lv08A2wo9CfyTA=="], + "@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.55.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FDBfT7yDGcspN0Cxbu/k8A0Pp1Jhv/m7BMTzXGpcb8ENl3tDj/51U65R5lWzUH15GaZA15HQ5A5wtafklxYj7g=="], - "@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.63.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-EvJb3aLiq1QedAZO4vqXTG0VJmKUpGU37r11thLPuL5HNa08sUS9DbF69RB8YoXVby2pXkFPMnbG0Pky0JMlKA=="], + "@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.64.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pFlCJjweTqVp7B220mCvCld1c1eYKZfQt1p3bxSbcReypKLJTwat+wbL2YZoX9jPi5X2O8tTKFEOahO5ehQGsA=="], - "@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.56.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1xBjUpDSJFZS4qYc4XXef0pzV38iHyKymY4sKQ3xPv7dGdka4We1PsuEg6Z8K21f1d2Yg5eU0OXXRSPVmowKfA=="], + "@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.57.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MthiekrU/BAJc5JZoZeJmo0OTX6ycJMiP6sMOSRTkvz5BrPMYDqaJos0OgsLPL/HpcgHP7eo5pduETuLguOqcg=="], - "@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.56.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-osdGMB3vc4bm1Kos04zfVmYAKoKVbKiF/Ti5/R0upDEOsCnrnUm9xvLeaKKbbE2WgJoaFz3VS8c99wx31efytQ=="], + "@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HFS/+FcZ6Q7piM7Il7CzQ4VHhJvGMJWjx7EgCkP5AnTntSN5rb5Xi3TkYJHBKeR27A0QqPlGaCITi93fUDs++Q=="], - "@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.56.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rW0hIpoaCFf55j0F1oqw6+Xv9IQeqJGtw9MudT3LCuhqld9S3DF0UEj8o3CZuPhcYqD+HAivZQdrsO5XMWyFqw=="], + "@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nHSrYAwF7+aV1E1V9yOOP9TchOodb6fjn4gFvdrdQXiRE7cMuffyLLbCZlZd4wsspBzVwOXX8mpURdRserAhNA=="], - "@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.62.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-/ZSMRCyFRMjQVx7Wf+BIAOMEdN/XWBbAGTNLKfQgGYs1GlmdiIFkUy8Z8XGkToMpKrgZju0drlTQpqt4Ul7R6w=="], + "@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.63.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-dKm/ODNN3GgIQVlbD6ZPxwRc3kleLf95hrRWXM+l8wYo+vSeXtEpQPT53afEf6VFWDVzJK55VGn8KMLtSve/cg=="], - "@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.58.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-tOGxw+6HZ5LDpMP05zYKtTw5HPqf3PXYHaOuN+pkv6uIgrZ+gTT75ELkd49eXBpjg3t36p8bYpsLgYcpIPqWqA=="], + "@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.59.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JKv1KDDYA2chJ1PC3pLP+Q9ISMQk6h5ey+99mB57/ARk0vQPGZTTEb4h4/JlcEpy7AYT8HIGv7X6l+br03Neeg=="], - "@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.29.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Jtnayb074lk7DQL25pOOpjvg4zjJMFjFWOLlKzTF5i1KxMR4+GlR/DSYgwDRfc0a4sfPXzdb/yYw7jRSX/LdFg=="], + "@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.30.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bZy9Q8jFdycKQ2pAsyuHYUHNmCxCOGdG6eg1Mn75RvQDccq832sU5OWOBnc12EFUELI6icJkhR7+EQKMBam2GA=="], - "@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.20.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-VGBQ89Bza1pKtV12Lxgv3uMrJ1vNcf1cDV6LAXp2wa6hnl6+IN6lbEmPn6WNWpguZTZaFEvugyZgN8FJuTjLEA=="], + "@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.21.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-gok0LPUOTz2FQ1YJMZzaHcOzDFyT64XJ8M9rNkugk923/p6lDGms/cRW1cqgqp6N6qcd6K6YdVHwPEhnx9BWbw=="], "@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.2", "", {}, "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA=="], @@ -221,6 +223,8 @@ "@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@2.23.1", "", {}, "sha512-l1z8AvI6k9I+2z49OgvP3SlzB1M0Lw24KtceiJibNaSyQwxsItoT9/XftZ/8BBtkosVmNOTQhL1eUsSkuSv1LA=="], + "@sentry/bun": ["@sentry/bun@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0", "@sentry/node": "10.38.0" } }, "sha512-8a2s+FVeqI2l12RNMFFEjAXpAUkqNZeGXTvHtjzcyWASW9szBNhOpiKN8oy0R/wUeIWgHpdnUeOSBhVKzH5YfQ=="], + "@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@2.23.1", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "2.23.1", "@sentry/cli": "2.39.1", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-JA6utNiwMKv6Jfj0Hmk0DI/XUizSHg7HhhkFETKhRlYEhZAdkyz1atDBg0ncKNgRBKyHeSYWcMFtUyo26VB76w=="], "@sentry/cli": ["@sentry/cli@2.39.1", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.39.1", "@sentry/cli-linux-arm": "2.39.1", "@sentry/cli-linux-arm64": "2.39.1", "@sentry/cli-linux-i686": "2.39.1", "@sentry/cli-linux-x64": "2.39.1", "@sentry/cli-win32-i686": "2.39.1", "@sentry/cli-win32-x64": "2.39.1" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ=="], @@ -239,15 +243,15 @@ "@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.39.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw=="], - "@sentry/core": ["@sentry/core@10.36.0", "", {}, "sha512-EYJjZvofI+D93eUsPLDIUV0zQocYqiBRyXS6CCV6dHz64P/Hob5NJQOwPa8/v6nD+UvJXvwsFfvXOHhYZhZJOQ=="], + "@sentry/core": ["@sentry/core@10.38.0", "", {}, "sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g=="], "@sentry/esbuild-plugin": ["@sentry/esbuild-plugin@2.23.1", "", { "dependencies": { "@sentry/bundler-plugin-core": "2.23.1", "unplugin": "1.0.1", "uuid": "^9.0.0" } }, "sha512-iGJRYbqhTokR7AMM5phA/z47141Mh5lhWeb60mmNlFvDky4cTISs2l8rtgu1GSARbzskVaA3NW16DIagX7OddA=="], - "@sentry/node": ["@sentry/node@10.36.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.4.0", "@opentelemetry/core": "^2.4.0", "@opentelemetry/instrumentation": "^0.210.0", "@opentelemetry/instrumentation-amqplib": "0.57.0", "@opentelemetry/instrumentation-connect": "0.53.0", "@opentelemetry/instrumentation-dataloader": "0.27.0", "@opentelemetry/instrumentation-express": "0.58.0", "@opentelemetry/instrumentation-fs": "0.29.0", "@opentelemetry/instrumentation-generic-pool": "0.53.0", "@opentelemetry/instrumentation-graphql": "0.57.0", "@opentelemetry/instrumentation-hapi": "0.56.0", "@opentelemetry/instrumentation-http": "0.210.0", "@opentelemetry/instrumentation-ioredis": "0.58.0", "@opentelemetry/instrumentation-kafkajs": "0.19.0", "@opentelemetry/instrumentation-knex": "0.54.0", "@opentelemetry/instrumentation-koa": "0.58.0", "@opentelemetry/instrumentation-lru-memoizer": "0.54.0", "@opentelemetry/instrumentation-mongodb": "0.63.0", "@opentelemetry/instrumentation-mongoose": "0.56.0", "@opentelemetry/instrumentation-mysql": "0.56.0", "@opentelemetry/instrumentation-mysql2": "0.56.0", "@opentelemetry/instrumentation-pg": "0.62.0", "@opentelemetry/instrumentation-redis": "0.58.0", "@opentelemetry/instrumentation-tedious": "0.29.0", "@opentelemetry/instrumentation-undici": "0.20.0", "@opentelemetry/resources": "^2.4.0", "@opentelemetry/sdk-trace-base": "^2.4.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "7.2.0", "@sentry/core": "10.36.0", "@sentry/node-core": "10.36.0", "@sentry/opentelemetry": "10.36.0", "import-in-the-middle": "^2.0.1", "minimatch": "^9.0.0" } }, "sha512-c7kYTZ9WcOYqod65PpA4iY+wEGJqLbFy10v4lIG6B5XrO+PFEXh1CrvGPLDJVogbB/4NE0r2jgeFQ+jz8aZUhw=="], + "@sentry/node": ["@sentry/node@10.38.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.5.0", "@opentelemetry/core": "^2.5.0", "@opentelemetry/instrumentation": "^0.211.0", "@opentelemetry/instrumentation-amqplib": "0.58.0", "@opentelemetry/instrumentation-connect": "0.54.0", "@opentelemetry/instrumentation-dataloader": "0.28.0", "@opentelemetry/instrumentation-express": "0.59.0", "@opentelemetry/instrumentation-fs": "0.30.0", "@opentelemetry/instrumentation-generic-pool": "0.54.0", "@opentelemetry/instrumentation-graphql": "0.58.0", "@opentelemetry/instrumentation-hapi": "0.57.0", "@opentelemetry/instrumentation-http": "0.211.0", "@opentelemetry/instrumentation-ioredis": "0.59.0", "@opentelemetry/instrumentation-kafkajs": "0.20.0", "@opentelemetry/instrumentation-knex": "0.55.0", "@opentelemetry/instrumentation-koa": "0.59.0", "@opentelemetry/instrumentation-lru-memoizer": "0.55.0", "@opentelemetry/instrumentation-mongodb": "0.64.0", "@opentelemetry/instrumentation-mongoose": "0.57.0", "@opentelemetry/instrumentation-mysql": "0.57.0", "@opentelemetry/instrumentation-mysql2": "0.57.0", "@opentelemetry/instrumentation-pg": "0.63.0", "@opentelemetry/instrumentation-redis": "0.59.0", "@opentelemetry/instrumentation-tedious": "0.30.0", "@opentelemetry/instrumentation-undici": "0.21.0", "@opentelemetry/resources": "^2.5.0", "@opentelemetry/sdk-trace-base": "^2.5.0", "@opentelemetry/semantic-conventions": "^1.39.0", "@prisma/instrumentation": "7.2.0", "@sentry/core": "10.38.0", "@sentry/node-core": "10.38.0", "@sentry/opentelemetry": "10.38.0", "import-in-the-middle": "^2.0.6", "minimatch": "^9.0.0" } }, "sha512-wriyDtWDAoatn8EhOj0U4PJR1WufiijTsCGALqakOHbFiadtBJANLe6aSkXoXT4tegw59cz1wY4NlzHjYksaPw=="], - "@sentry/node-core": ["@sentry/node-core@10.36.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.36.0", "@sentry/opentelemetry": "10.36.0", "import-in-the-middle": "^2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-3K2SJCPiQGQMYSVSF3GuPIAilJPlXOWxyvrmnxY9Zw3ZbXaLynhYCJ5TjL38hS7XoMby/0lN2fY/kbXH/GlNeg=="], + "@sentry/node-core": ["@sentry/node-core@10.38.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.38.0", "@sentry/opentelemetry": "10.38.0", "import-in-the-middle": "^2.0.6" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-ErXtpedrY1HghgwM6AliilZPcUCoNNP1NThdO4YpeMq04wMX9/GMmFCu46TnCcg6b7IFIOSr2S4yD086PxLlHQ=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@10.36.0", "", { "dependencies": { "@sentry/core": "10.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-TPOSiHBk45exA/LGFELSuzmBrWe1MG7irm7NkUXCZfdXuLLPeUtp1Y+rWDCWWNMrraAdizDN0d/l1GSLpxzpPg=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.38.0", "", { "dependencies": { "@sentry/core": "10.38.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-YPVhWfYmC7nD3EJqEHGtjp4fp5LwtAbE5rt9egQ4hqJlYFvr8YEz9sdoqSZxO0cZzgs2v97HFl/nmWAXe52G2Q=="], "@stricli/auto-complete": ["@stricli/auto-complete@1.2.5", "", { "dependencies": { "@stricli/core": "^1.2.5" }, "bin": { "auto-complete": "dist/bin/cli.js" } }, "sha512-C6G88Hh4lUWBwiqsxbcA4I1ricSQwiLaOziTWW3NmBoX7WGTW7i7RvyooXMpZk1YMLf2olv5Odxmg127ik1DKQ=="], @@ -339,7 +343,7 @@ "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], + "import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], @@ -447,6 +451,8 @@ "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "uuidv7": ["uuidv7@1.1.0", "", { "bin": { "uuidv7": "cli.js" } }, "sha512-2VNnOC0+XQlwogChUDzy6pe8GQEys9QFZBGOh54l6qVfwoCUwwRvk7rDTgaIsRgsF5GFa5oiNg8LqXE3jofBBg=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="], @@ -467,8 +473,6 @@ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.4.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw=="], - "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], "@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -485,6 +489,8 @@ "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], + "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], "@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], diff --git a/package.json b/package.json index 2e483eb1..bcf671e5 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ }, "devDependencies": { "@biomejs/biome": "2.3.8", + "@sentry/bun": "10.38.0", "@sentry/esbuild-plugin": "^2.23.0", - "@sentry/node": "^10.36.0", + "@sentry/node": "10.38.0", "@stricli/auto-complete": "^1.2.4", "@stricli/core": "^1.2.4", "@types/bun": "latest", @@ -39,6 +40,7 @@ "tinyglobby": "^0.2.15", "typescript": "^5", "ultracite": "6.3.10", + "uuidv7": "^1.1.0", "zod": "^3.24.0" }, "repository": { @@ -52,6 +54,6 @@ "packageManager": "bun@1.3.3", "patchedDependencies": { "@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch", - "@sentry/core@10.36.0": "patches/@sentry%2Fcore@10.36.0.patch" + "@sentry/core@10.38.0": "patches/@sentry%2Fcore@10.38.0.patch" } } diff --git a/patches/@sentry%2Fcore@10.36.0.patch b/patches/@sentry%2Fcore@10.38.0.patch similarity index 88% rename from patches/@sentry%2Fcore@10.36.0.patch rename to patches/@sentry%2Fcore@10.38.0.patch index 11982397..68050c0f 100644 --- a/patches/@sentry%2Fcore@10.36.0.patch +++ b/patches/@sentry%2Fcore@10.38.0.patch @@ -1,8 +1,8 @@ -diff --git a/build/esm/client.js b/build/esm/client.js -index 1111111111111111111111111111111111111111..2222222222222222222222222222222222222222 100644 ---- a/build/esm/client.js -+++ b/build/esm/client.js -@@ -643,11 +643,16 @@ class Client { +diff --git a/build/cjs/client.js b/build/cjs/client.js +index 492d897927b0a791e0dac10647274e92053ac985..e253ad00bc5f858b62a9581d09688fe7a5d45f23 100644 +--- a/build/cjs/client.js ++++ b/build/cjs/client.js +@@ -645,11 +645,16 @@ class Client { * `false` otherwise */ async _isClientDoneProcessing(timeout) { @@ -20,11 +20,24 @@ index 1111111111111111111111111111111111111111..22222222222222222222222222222222 if (!this._numProcessing) { return true; -diff --git a/build/cjs/client.js b/build/cjs/client.js -index 3333333333333333333333333333333333333333..4444444444444444444444444444444444444444 100644 ---- a/build/cjs/client.js -+++ b/build/cjs/client.js -@@ -645,11 +645,16 @@ class Client { +diff --git a/build/cjs/utils/promisebuffer.js b/build/cjs/utils/promisebuffer.js +index 6413b77ec339af98b608c4609fc9462d9a8e967d..85a9af7221cfabcfe5c65df2f4ece5a294d34e61 100644 +--- a/build/cjs/utils/promisebuffer.js ++++ b/build/cjs/utils/promisebuffer.js +@@ -71,7 +71,7 @@ function makePromiseBuffer(limit = 100) { + return drainPromise; + } + +- const promises = [drainPromise, new Promise(resolve => setTimeout(() => resolve(false), timeout))]; ++ const promises = [drainPromise, new Promise(resolve => { const t = setTimeout(() => resolve(false), timeout); if (typeof t !== 'number' && t.unref) t.unref(); })]; + + // Promise.race will resolve to the first promise that resolves or rejects + // So if the drainPromise resolves, the timeout promise will be ignored +diff --git a/build/esm/client.js b/build/esm/client.js +index bcc614aa8485bd110abc51149370bbf114ac9e54..2677a9bb0b3d9451a0bd50926b3a36516664c69f 100644 +--- a/build/esm/client.js ++++ b/build/esm/client.js +@@ -643,11 +643,16 @@ class Client { * `false` otherwise */ async _isClientDoneProcessing(timeout) { @@ -43,7 +56,7 @@ index 3333333333333333333333333333333333333333..44444444444444444444444444444444 if (!this._numProcessing) { return true; diff --git a/build/esm/utils/promisebuffer.js b/build/esm/utils/promisebuffer.js -index 5555555555555555555555555555555555555555..6666666666666666666666666666666666666666 100644 +index b8111cf976c8338db28b5e9ff111870864b584cb..458ea7ccb0a2704df1c2c2ec5fb303cff37dc941 100644 --- a/build/esm/utils/promisebuffer.js +++ b/build/esm/utils/promisebuffer.js @@ -69,7 +69,7 @@ function makePromiseBuffer(limit = 100) { @@ -55,16 +68,3 @@ index 5555555555555555555555555555555555555555..66666666666666666666666666666666 // Promise.race will resolve to the first promise that resolves or rejects // So if the drainPromise resolves, the timeout promise will be ignored -diff --git a/build/cjs/utils/promisebuffer.js b/build/cjs/utils/promisebuffer.js -index 7777777777777777777777777777777777777777..8888888888888888888888888888888888888888 100644 ---- a/build/cjs/utils/promisebuffer.js -+++ b/build/cjs/utils/promisebuffer.js -@@ -71,7 +71,7 @@ function makePromiseBuffer(limit = 100) { - return drainPromise; - } - -- const promises = [drainPromise, new Promise(resolve => setTimeout(() => resolve(false), timeout))]; -+ const promises = [drainPromise, new Promise(resolve => { const t = setTimeout(() => resolve(false), timeout); if (typeof t !== 'number' && t.unref) t.unref(); })]; - - // Promise.race will resolve to the first promise that resolves or rejects - // So if the drainPromise resolves, the timeout promise will be ignored diff --git a/script/bundle.ts b/script/bundle.ts index 8bd2a6dc..ce069905 100644 --- a/script/bundle.ts +++ b/script/bundle.ts @@ -89,6 +89,10 @@ const result = await build({ entryPoints: ["./src/bin.ts"], bundle: true, minify: true, + // Replace @sentry/bun with @sentry/node for Node.js npm package + alias: { + "@sentry/bun": "@sentry/node", + }, banner: { // Suppress Node.js warnings (e.g., SQLite experimental) - not useful for CLI users js: `#!/usr/bin/env node diff --git a/script/node-polyfills.ts b/script/node-polyfills.ts index 85925ffc..94107509 100644 --- a/script/node-polyfills.ts +++ b/script/node-polyfills.ts @@ -6,6 +6,7 @@ import { access, readFile, writeFile } from "node:fs/promises"; import { DatabaseSync } from "node:sqlite"; import { glob } from "tinyglobby"; +import { uuidv7 } from "uuidv7"; declare global { var Bun: typeof BunPolyfill; @@ -139,6 +140,10 @@ const BunPolyfill = { } } }, + + randomUUIDv7(): string { + return uuidv7(); + }, }; globalThis.Bun = BunPolyfill as typeof Bun; diff --git a/src/bin.ts b/src/bin.ts index 00b6f05a..19358a5f 100755 --- a/src/bin.ts +++ b/src/bin.ts @@ -9,7 +9,9 @@ async function main(): Promise { const args = process.argv.slice(2); try { - await withTelemetry(async () => run(app, args, buildContext(process))); + await withTelemetry(async (span) => + run(app, args, buildContext(process, span)) + ); } catch (err) { process.stderr.write(`${error("Error:")} ${formatError(err)}\n`); process.exit(getExitCode(err)); diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index da2f5658..86d3be84 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -1,15 +1,39 @@ +// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import +import * as Sentry from "@sentry/bun"; import { buildCommand, numberParser } from "@stricli/core"; import type { SentryContext } from "../../context.js"; -import { listOrganizations } from "../../lib/api-client.js"; +import { getCurrentUser } from "../../lib/api-client.js"; import { openBrowser } from "../../lib/browser.js"; import { setupCopyKeyListener } from "../../lib/clipboard.js"; import { clearAuth, isAuthenticated, setAuthToken } from "../../lib/db/auth.js"; import { getDbPath } from "../../lib/db/index.js"; +import { setUserInfo } from "../../lib/db/user.js"; import { AuthError } from "../../lib/errors.js"; import { muted, success } from "../../lib/formatters/colors.js"; import { formatDuration } from "../../lib/formatters/human.js"; import { completeOAuthFlow, performDeviceFlow } from "../../lib/oauth.js"; import { generateQRCode } from "../../lib/qrcode.js"; +import type { SentryUser } from "../../types/index.js"; + +/** + * Format user identity for display. + * Handles missing username/email gracefully. + */ +function formatUserIdentity(user: SentryUser): string { + const { username, email, id } = user; + + if (username && email) { + return `${username} <${email}>`; + } + if (username) { + return username; + } + if (email) { + return email; + } + // Fallback to user ID if no username/email + return `user ${id}`; +} type LoginFlags = { readonly token?: string; @@ -54,12 +78,13 @@ export const loginCommand = buildCommand({ // Token-based authentication if (flags.token) { - // Save token first, then validate by making an API call + // Save token first, then validate by fetching user info await setAuthToken(flags.token); - // Validate the token by trying to list organizations + // Validate token by fetching user info + let user: SentryUser; try { - await listOrganizations(); + user = await getCurrentUser(); } catch { // Token is invalid - clear it and throw await clearAuth(); @@ -69,7 +94,20 @@ export const loginCommand = buildCommand({ ); } + // Store user info for telemetry (non-critical, don't block auth) + try { + setUserInfo({ + userId: user.id, + email: user.email, + username: user.username, + }); + } catch (error) { + // Report to Sentry but don't block auth - user info is not critical + Sentry.captureException(error); + } + stdout.write(`${success("✓")} Authenticated with API token\n`); + stdout.write(` Logged in as ${muted(formatUserIdentity(user))}\n`); stdout.write(` Config saved to: ${getDbPath()}\n`); return; } @@ -133,7 +171,24 @@ export const loginCommand = buildCommand({ // Store the token await completeOAuthFlow(tokenResponse); + // Fetch and store user info for telemetry + let user: SentryUser | undefined; + try { + user = await getCurrentUser(); + setUserInfo({ + userId: user.id, + email: user.email, + username: user.username, + }); + } catch (error) { + // Report to Sentry but don't block auth - user info is not critical + Sentry.captureException(error); + } + stdout.write(`${success("✓")} Authentication successful!\n`); + if (user) { + stdout.write(` Logged in as ${muted(formatUserIdentity(user))}\n`); + } stdout.write(` Config saved to: ${getDbPath()}\n`); if (tokenResponse.expires_in) { diff --git a/src/commands/issue/list.ts b/src/commands/issue/list.ts index 9f402262..bba08391 100644 --- a/src/commands/issue/list.ts +++ b/src/commands/issue/list.ts @@ -308,7 +308,7 @@ export const listCommand = buildCommand({ }, // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: command entry point with inherent complexity async func(this: SentryContext, flags: ListFlags): Promise { - const { stdout, cwd } = this; + const { stdout, cwd, setContext } = this; // Resolve targets (may find multiple in monorepos) const { targets, footer, skippedSelfHosted, detectedDsns } = @@ -319,6 +319,11 @@ export const listCommand = buildCommand({ usageHint: USAGE_HINT, }); + // Set telemetry context with unique orgs and projects + const orgs = [...new Set(targets.map((t) => t.org))]; + const projects = [...new Set(targets.map((t) => t.project))]; + setContext(orgs, projects); + if (targets.length === 0) { if (skippedSelfHosted) { throw new ContextError( diff --git a/src/commands/issue/view.ts b/src/commands/issue/view.ts index 62eb84b4..dc1503ba 100644 --- a/src/commands/issue/view.ts +++ b/src/commands/issue/view.ts @@ -157,7 +157,7 @@ export const viewCommand = buildCommand({ flags: ViewFlags, issueId: string ): Promise { - const { stdout, cwd } = this; + const { stdout, cwd, setContext } = this; // Resolve issue using shared resolution logic const { org: orgSlug, issue } = await resolveIssue({ @@ -168,6 +168,12 @@ export const viewCommand = buildCommand({ commandHint: buildCommandHint("view", issueId), }); + // Set telemetry context + setContext( + orgSlug ? [orgSlug] : [], + issue.project?.slug ? [issue.project.slug] : [] + ); + if (flags.web) { await openInBrowser(stdout, issue.permalink, "issue"); return; diff --git a/src/context.ts b/src/context.ts index 2611fb69..0e74107f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -8,7 +8,11 @@ import { homedir } from "node:os"; import type { CommandContext } from "@stricli/core"; import { getConfigDir } from "./lib/db/index.js"; -import { setCommandName } from "./lib/telemetry.js"; +import { + type Span, + setCommandSpanName, + setOrgProjectContext, +} from "./lib/telemetry.js"; import type { Writer } from "./types/index.js"; export interface SentryContext extends CommandContext { @@ -20,6 +24,13 @@ export interface SentryContext extends CommandContext { readonly stdout: Writer; readonly stderr: Writer; readonly stdin: NodeJS.ReadStream & { fd: 0 }; + /** + * Set organization and project context for telemetry. + * Call this after resolving the target org/project to enable + * filtering by org/project in Sentry. + * Accepts arrays to support multi-project commands. + */ + readonly setContext: (orgs: string[], projects: string[]) => void; } /** @@ -27,9 +38,12 @@ export interface SentryContext extends CommandContext { * * The forCommand method is called by stricli with the command prefix * (e.g., ["auth", "login"]) before running the command. + * + * @param process - The Node.js process object + * @param span - The telemetry span from withTelemetry (optional) */ -export function buildContext(process: NodeJS.Process) { - const baseContext = { +export function buildContext(process: NodeJS.Process, span?: Span) { + const baseContext: SentryContext = { process, env: process.env, cwd: process.cwd(), @@ -38,12 +52,13 @@ export function buildContext(process: NodeJS.Process) { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin, + setContext: setOrgProjectContext, }; return { ...baseContext, forCommand: ({ prefix }: { prefix: readonly string[] }): SentryContext => { - setCommandName(prefix.join(".")); + setCommandSpanName(span, prefix.join(".")); return baseContext; }, }; diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index a3ea833e..ec7f939a 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -16,6 +16,8 @@ import { SentryOrganizationSchema, type SentryProject, SentryProjectSchema, + type SentryUser, + SentryUserSchema, type TraceResponse, type TraceSpan, } from "../types/index.js"; @@ -23,6 +25,7 @@ import type { AutofixResponse, AutofixState } from "../types/seer.js"; import { getUserAgent } from "./constants.js"; import { refreshToken } from "./db/auth.js"; import { ApiError } from "./errors.js"; +import { withHttpSpan } from "./telemetry.js"; const DEFAULT_SENTRY_URL = "https://sentry.io"; @@ -180,49 +183,52 @@ export function buildSearchParams( * @throws {ApiError} On API errors * @throws {z.ZodError} When response fails schema validation */ -export async function apiRequest( +export function apiRequest( endpoint: string, options: ApiRequestOptions = {} ): Promise { const { method = "GET", body, params, schema } = options; - const client = await createApiClient(); - let response: Response; - try { - response = await client(normalizePath(endpoint), { - method, - json: body, - searchParams: buildSearchParams(params), - }); - } catch (error) { - // Transform ky HTTPError into ApiError - if (error && typeof error === "object" && "response" in error) { - const kyError = error as { response: Response }; - const text = await kyError.response.text(); - let detail: string | undefined; - try { - const parsed = JSON.parse(text) as { detail?: string }; - detail = parsed.detail ?? JSON.stringify(parsed); - } catch { - detail = text; + return withHttpSpan(method, endpoint, async () => { + const client = await createApiClient(); + + let response: Response; + try { + response = await client(normalizePath(endpoint), { + method, + json: body, + searchParams: buildSearchParams(params), + }); + } catch (error) { + // Transform ky HTTPError into ApiError + if (error && typeof error === "object" && "response" in error) { + const kyError = error as { response: Response }; + const text = await kyError.response.text(); + let detail: string | undefined; + try { + const parsed = JSON.parse(text) as { detail?: string }; + detail = parsed.detail ?? JSON.stringify(parsed); + } catch { + detail = text; + } + throw new ApiError( + `API request failed: ${kyError.response.status} ${kyError.response.statusText}`, + kyError.response.status, + detail + ); } - throw new ApiError( - `API request failed: ${kyError.response.status} ${kyError.response.statusText}`, - kyError.response.status, - detail - ); + throw error; } - throw error; - } - const data = await response.json(); + const data = await response.json(); - // Validate response if schema provided - if (schema) { - return schema.parse(data); - } + // Validate response if schema provided + if (schema) { + return schema.parse(data); + } - return data as T; + return data as T; + }); } /** @@ -235,60 +241,63 @@ export async function apiRequest( * @returns Response status, headers, and parsed body * @throws {AuthError} Only on authentication failure (not on API errors) */ -export async function rawApiRequest( +export function rawApiRequest( endpoint: string, options: ApiRequestOptions & { headers?: Record } = {} ): Promise<{ status: number; headers: Headers; body: unknown }> { const { method = "GET", body, params, headers: customHeaders = {} } = options; - const client = await createApiClient(); - - // Handle body based on type: - // - Objects: use ky's json option (auto-stringifies and sets Content-Type) - // - Strings: send as raw body (user can set Content-Type via custom headers if needed) - // - undefined: no body - const isStringBody = typeof body === "string"; - - // For string bodies, remove the default Content-Type: application/json from createApiClient - // unless the user explicitly provides one. This allows sending non-JSON content. - // Check is case-insensitive since HTTP headers are case-insensitive. - const hasContentType = Object.keys(customHeaders).some( - (k) => k.toLowerCase() === "content-type" - ); - const headers = - isStringBody && !hasContentType - ? { ...customHeaders, "Content-Type": undefined } - : customHeaders; - - const requestOptions: Parameters[1] = { - method, - searchParams: buildSearchParams(params), - headers, - throwHttpErrors: false, - }; - - if (body !== undefined) { - if (isStringBody) { - requestOptions.body = body; - } else { - requestOptions.json = body; + + return withHttpSpan(method, endpoint, async () => { + const client = await createApiClient(); + + // Handle body based on type: + // - Objects: use ky's json option (auto-stringifies and sets Content-Type) + // - Strings: send as raw body (user can set Content-Type via custom headers if needed) + // - undefined: no body + const isStringBody = typeof body === "string"; + + // For string bodies, remove the default Content-Type: application/json from createApiClient + // unless the user explicitly provides one. This allows sending non-JSON content. + // Check is case-insensitive since HTTP headers are case-insensitive. + const hasContentType = Object.keys(customHeaders).some( + (k) => k.toLowerCase() === "content-type" + ); + const headers = + isStringBody && !hasContentType + ? { ...customHeaders, "Content-Type": undefined } + : customHeaders; + + const requestOptions: Parameters[1] = { + method, + searchParams: buildSearchParams(params), + headers, + throwHttpErrors: false, + }; + + if (body !== undefined) { + if (isStringBody) { + requestOptions.body = body; + } else { + requestOptions.json = body; + } } - } - const response = await client(normalizePath(endpoint), requestOptions); + const response = await client(normalizePath(endpoint), requestOptions); - const text = await response.text(); - let responseBody: unknown; - try { - responseBody = JSON.parse(text); - } catch { - responseBody = text; - } + const text = await response.text(); + let responseBody: unknown; + try { + responseBody = JSON.parse(text); + } catch { + responseBody = text; + } - return { - status: response.status, - headers: response.headers, - body: responseBody, - }; + return { + status: response.status, + headers: response.headers, + body: responseBody, + }; + }); } // ───────────────────────────────────────────────────────────────────────────── @@ -572,3 +581,13 @@ export function triggerSolutionPlanning( }, }); } + +/** + * Get the currently authenticated user's information. + * Used for setting user context in telemetry. + */ +export function getCurrentUser(): Promise { + return apiRequest("/users/me/", { + schema: SentryUserSchema, + }); +} diff --git a/src/lib/db/auth.ts b/src/lib/db/auth.ts index 8a0331ad..752a9886 100644 --- a/src/lib/db/auth.ts +++ b/src/lib/db/auth.ts @@ -2,6 +2,7 @@ * Authentication credential storage (single-row table pattern). */ +import { withDbSpan } from "../telemetry.js"; import { getDatabase } from "./index.js"; /** Refresh when less than 10% of token lifetime remains */ @@ -25,67 +26,77 @@ export type AuthConfig = { issuedAt?: number; }; -export async function getAuthConfig(): Promise { - const db = getDatabase(); - const row = db.query("SELECT * FROM auth WHERE id = 1").get() as - | AuthRow - | undefined; +export function getAuthConfig(): AuthConfig | undefined { + return withDbSpan("getAuthConfig", () => { + const db = getDatabase(); + const row = db.query("SELECT * FROM auth WHERE id = 1").get() as + | AuthRow + | undefined; - if (!row?.token) { - return; - } + if (!row?.token) { + return; + } - return { - token: row.token ?? undefined, - refreshToken: row.refresh_token ?? undefined, - expiresAt: row.expires_at ?? undefined, - issuedAt: row.issued_at ?? undefined, - }; + return { + token: row.token ?? undefined, + refreshToken: row.refresh_token ?? undefined, + expiresAt: row.expires_at ?? undefined, + issuedAt: row.issued_at ?? undefined, + }; + }); } /** Get the stored token, or undefined if expired. Use refreshToken() for auto-refresh. */ -export async function getAuthToken(): Promise { - const db = getDatabase(); - const row = db.query("SELECT * FROM auth WHERE id = 1").get() as - | AuthRow - | undefined; - - if (!row?.token) { - return; - } +export function getAuthToken(): string | undefined { + return withDbSpan("getAuthToken", () => { + const db = getDatabase(); + const row = db.query("SELECT * FROM auth WHERE id = 1").get() as + | AuthRow + | undefined; + + if (!row?.token) { + return; + } - if (row.expires_at && Date.now() > row.expires_at) { - return; - } + if (row.expires_at && Date.now() > row.expires_at) { + return; + } - return row.token; + return row.token; + }); } -export async function setAuthToken( +export function setAuthToken( token: string, expiresIn?: number, newRefreshToken?: string -): Promise { - const db = getDatabase(); - const now = Date.now(); - const expiresAt = expiresIn ? now + expiresIn * 1000 : null; - const issuedAt = expiresIn ? now : null; - - db.query(` - INSERT INTO auth (id, token, refresh_token, expires_at, issued_at, updated_at) - VALUES (1, ?, ?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET - token = excluded.token, - refresh_token = excluded.refresh_token, - expires_at = excluded.expires_at, - issued_at = excluded.issued_at, - updated_at = excluded.updated_at - `).run(token, newRefreshToken ?? null, expiresAt, issuedAt, now); +): void { + withDbSpan("setAuthToken", () => { + const db = getDatabase(); + const now = Date.now(); + const expiresAt = expiresIn ? now + expiresIn * 1000 : null; + const issuedAt = expiresIn ? now : null; + + db.query(` + INSERT INTO auth (id, token, refresh_token, expires_at, issued_at, updated_at) + VALUES (1, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + token = excluded.token, + refresh_token = excluded.refresh_token, + expires_at = excluded.expires_at, + issued_at = excluded.issued_at, + updated_at = excluded.updated_at + `).run(token, newRefreshToken ?? null, expiresAt, issuedAt, now); + }); } -export async function clearAuth(): Promise { - const db = getDatabase(); - db.query("DELETE FROM auth WHERE id = 1").run(); +export function clearAuth(): void { + withDbSpan("clearAuth", () => { + const db = getDatabase(); + db.query("DELETE FROM auth WHERE id = 1").run(); + // Also clear user info when logging out + db.query("DELETE FROM user_info WHERE id = 1").run(); + }); } export async function isAuthenticated(): Promise { diff --git a/src/lib/db/instance.ts b/src/lib/db/instance.ts new file mode 100644 index 00000000..137f2f27 --- /dev/null +++ b/src/lib/db/instance.ts @@ -0,0 +1,46 @@ +/** + * Instance identifier for telemetry. + * + * Generates and persists a unique identifier for this CLI installation. + * Uses UUIDv7 for time-sortable, unique identifiers. + */ + +import { getDatabase } from "./index.js"; + +/** + * Get the instance ID, generating one if it doesn't exist. + * + * The instance ID is generated once on first access and persisted + * in the database. It identifies this CLI installation for telemetry. + */ +export function getInstanceId(): string { + const db = getDatabase(); + + // Try to get existing instance ID + const existingRow = db + .query("SELECT instance_id FROM instance_info WHERE id = 1") + .get() as { instance_id: string } | undefined; + + if (existingRow) { + return existingRow.instance_id; + } + + // Generate and store new instance ID + // Use INSERT OR IGNORE to handle race condition when multiple CLI processes + // start simultaneously - only the first insert succeeds + // Bun.randomUUIDv7() is native in Bun, polyfilled via uuidv7 package for Node.js + const instanceId = Bun.randomUUIDv7(); + const now = Date.now(); + + db.query(` + INSERT OR IGNORE INTO instance_info (id, instance_id, created_at) + VALUES (1, ?, ?) + `).run(instanceId, now); + + // Re-fetch to get the actual stored value (may differ if another process won the race) + const row = db + .query("SELECT instance_id FROM instance_info WHERE id = 1") + .get() as { instance_id: string }; + + return row.instance_id; +} diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 291d4472..3f8d28cf 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -4,7 +4,27 @@ import type { Database } from "bun:sqlite"; -const CURRENT_SCHEMA_VERSION = 1; +const CURRENT_SCHEMA_VERSION = 2; + +/** User identity for telemetry (single row, id=1) */ +const USER_INFO_TABLE = ` + CREATE TABLE IF NOT EXISTS user_info ( + id INTEGER PRIMARY KEY CHECK (id = 1), + user_id TEXT NOT NULL, + email TEXT, + username TEXT, + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) + ) +`; + +/** Instance identifier for telemetry (single row, id=1) */ +const INSTANCE_INFO_TABLE = ` + CREATE TABLE IF NOT EXISTS instance_info ( + id INTEGER PRIMARY KEY CHECK (id = 1), + instance_id TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) + ) +`; export function initSchema(db: Database): void { db.exec(` @@ -74,6 +94,9 @@ export function initSchema(db: Database): void { key TEXT PRIMARY KEY, value TEXT NOT NULL ); + + ${USER_INFO_TABLE}; + ${INSTANCE_INFO_TABLE}; `); const versionRow = db @@ -99,7 +122,12 @@ function getSchemaVersion(db: Database): number { export function runMigrations(db: Database): void { const currentVersion = getSchemaVersion(db); - // Add migrations here as schema evolves + // Migration from v1 to v2: Add user_info and instance_info tables + if (currentVersion < 2) { + db.exec(`${USER_INFO_TABLE}; ${INSTANCE_INFO_TABLE};`); + } + + // Update schema version if needed if (currentVersion < CURRENT_SCHEMA_VERSION) { db.query("UPDATE schema_version SET version = ?").run( CURRENT_SCHEMA_VERSION diff --git a/src/lib/db/user.ts b/src/lib/db/user.ts new file mode 100644 index 00000000..142b8c6d --- /dev/null +++ b/src/lib/db/user.ts @@ -0,0 +1,59 @@ +/** + * User identity storage for telemetry. + * + * Stores user info fetched from Sentry API to set Sentry user context. + */ + +import { getDatabase } from "./index.js"; + +export type UserInfo = { + userId: string; + email?: string; + username?: string; +}; + +type UserRow = { + user_id: string; + email: string | null; + username: string | null; +}; + +/** + * Get stored user info. + * Returns undefined if no user info is stored. + */ +export function getUserInfo(): UserInfo | undefined { + const db = getDatabase(); + const row = db.query("SELECT * FROM user_info WHERE id = 1").get() as + | UserRow + | undefined; + + if (!row) { + return; + } + + return { + userId: row.user_id, + email: row.email ?? undefined, + username: row.username ?? undefined, + }; +} + +/** + * Store user info. + * Overwrites any existing user info. + */ +export function setUserInfo(info: UserInfo): void { + const db = getDatabase(); + const now = Date.now(); + + db.query(` + INSERT INTO user_info (id, user_id, email, username, updated_at) + VALUES (1, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + user_id = excluded.user_id, + email = excluded.email, + username = excluded.username, + updated_at = excluded.updated_at + `).run(info.userId, info.email ?? null, info.username ?? null, now); +} diff --git a/src/lib/formatters/human.ts b/src/lib/formatters/human.ts index bad54b4c..a3d3fbb3 100644 --- a/src/lib/formatters/human.ts +++ b/src/lib/formatters/human.ts @@ -22,6 +22,7 @@ import type { TraceResponse, TraceSpan, } from "../../types/index.js"; +import { withSerializeSpan } from "../telemetry.js"; import { boldUnderline, green, @@ -969,29 +970,31 @@ function formatTraceEventAsTree(traceEvent: TraceEvent): string[] { * @returns Array of formatted lines ready for display */ export function formatSpanTree(traceResponse: TraceResponse): string[] { - const traceEvents = traceResponse.transactions; + return withSerializeSpan("formatSpanTree", () => { + const traceEvents = traceResponse.transactions; - if (traceEvents.length === 0) { - return [muted("\nNo span data available.")]; - } + if (traceEvents.length === 0) { + return [muted("\nNo span data available.")]; + } - const lines: string[] = []; - lines.push(""); - lines.push(muted("─── Span Tree ───")); - lines.push(""); + const lines: string[] = []; + lines.push(""); + lines.push(muted("─── Span Tree ───")); + lines.push(""); - const sorted = [...traceEvents].sort((a, b) => { - const aStart = a.start_timestamp ?? 0; - const bStart = b.start_timestamp ?? 0; - return aStart - bStart; - }); + const sorted = [...traceEvents].sort((a, b) => { + const aStart = a.start_timestamp ?? 0; + const bStart = b.start_timestamp ?? 0; + return aStart - bStart; + }); - for (const traceEvent of sorted) { - lines.push(...formatTraceEventAsTree(traceEvent)); - lines.push(""); - } + for (const traceEvent of sorted) { + lines.push(...formatTraceEventAsTree(traceEvent)); + lines.push(""); + } - return lines; + return lines; + }); } // ───────────────────────────────────────────────────────────────────────────── @@ -1049,27 +1052,29 @@ export function formatSimpleSpanTree( spans: TraceSpan[], maxDepth = Number.MAX_SAFE_INTEGER ): string[] { - if (spans.length === 0) { - return [muted("No span data available.")]; - } + return withSerializeSpan("formatSimpleSpanTree", () => { + if (spans.length === 0) { + return [muted("No span data available.")]; + } - const effectiveMaxDepth = maxDepth > 0 ? maxDepth : Number.MAX_SAFE_INTEGER; + const effectiveMaxDepth = maxDepth > 0 ? maxDepth : Number.MAX_SAFE_INTEGER; - const lines: string[] = []; - lines.push(`${muted("Trace —")} ${traceId}`); - - const spanCount = spans.length; - spans.forEach((span, i) => { - formatSpanSimple(span, { - lines, - prefix: "", - isLast: i === spanCount - 1, - currentDepth: 1, - maxDepth: effectiveMaxDepth, + const lines: string[] = []; + lines.push(`${muted("Trace —")} ${traceId}`); + + const spanCount = spans.length; + spans.forEach((span, i) => { + formatSpanSimple(span, { + lines, + prefix: "", + isLast: i === spanCount - 1, + currentDepth: 1, + maxDepth: effectiveMaxDepth, + }); }); - }); - return lines; + return lines; + }); } // ───────────────────────────────────────────────────────────────────────────── @@ -1238,77 +1243,82 @@ export function formatEventDetails( header = "Latest Event", issuePermalink?: string ): string[] { - const lines: string[] = []; + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Event formatting requires multiple conditional sections + return withSerializeSpan("formatEventDetails", () => { + const lines: string[] = []; - // Header - lines.push(""); - lines.push(muted(`─── ${header} (${event.eventID.slice(0, 8)}) ───`)); - lines.push(""); + // Header + lines.push(""); + lines.push(muted(`─── ${header} (${event.eventID.slice(0, 8)}) ───`)); + lines.push(""); - // Basic info - lines.push(`Event ID: ${event.eventID}`); - if (event.dateReceived) { - lines.push(`Received: ${new Date(event.dateReceived).toLocaleString()}`); - } - if (event.location) { - lines.push(`Location: ${event.location}`); - } + // Basic info + lines.push(`Event ID: ${event.eventID}`); + if (event.dateReceived) { + lines.push( + `Received: ${new Date(event.dateReceived).toLocaleString()}` + ); + } + if (event.location) { + lines.push(`Location: ${event.location}`); + } - // Trace context - const traceCtx = event.contexts?.trace; - if (traceCtx?.trace_id) { - lines.push(`Trace: ${traceCtx.trace_id}`); - } + // Trace context + const traceCtx = event.contexts?.trace; + if (traceCtx?.trace_id) { + lines.push(`Trace: ${traceCtx.trace_id}`); + } - // User info (including geo) - lines.push(...formatUserInfo(event)); + // User info (including geo) + lines.push(...formatUserInfo(event)); - // Environment contexts (browser, OS, device) - lines.push(...formatEnvironmentContexts(event)); + // Environment contexts (browser, OS, device) + lines.push(...formatEnvironmentContexts(event)); - // HTTP Request - const requestEntry = extractEntry(event, "request"); - if (requestEntry) { - lines.push(...formatRequest(requestEntry)); - } + // HTTP Request + const requestEntry = extractEntry(event, "request"); + if (requestEntry) { + lines.push(...formatRequest(requestEntry)); + } - // SDK info - if (event.sdk) { - lines.push(""); - lines.push(`SDK: ${event.sdk.name} ${event.sdk.version}`); - } + // SDK info + if (event.sdk) { + lines.push(""); + lines.push(`SDK: ${event.sdk.name} ${event.sdk.version}`); + } - // Release info - if (event.release?.shortVersion) { - lines.push(`Release: ${event.release.shortVersion}`); - } + // Release info + if (event.release?.shortVersion) { + lines.push(`Release: ${event.release.shortVersion}`); + } - // Stack Trace - const exceptionEntry = extractEntry(event, "exception"); - if (exceptionEntry) { - lines.push(...formatStackTrace(exceptionEntry)); - } + // Stack Trace + const exceptionEntry = extractEntry(event, "exception"); + if (exceptionEntry) { + lines.push(...formatStackTrace(exceptionEntry)); + } - // Breadcrumbs - const breadcrumbsEntry = extractEntry(event, "breadcrumbs"); - if (breadcrumbsEntry) { - lines.push(...formatBreadcrumbs(breadcrumbsEntry)); - } + // Breadcrumbs + const breadcrumbsEntry = extractEntry(event, "breadcrumbs"); + if (breadcrumbsEntry) { + lines.push(...formatBreadcrumbs(breadcrumbsEntry)); + } - // Replay link - lines.push(...formatReplayLink(event, issuePermalink)); + // Replay link + lines.push(...formatReplayLink(event, issuePermalink)); - // Tags - if (event.tags?.length) { - lines.push(""); - lines.push(muted("─── Tags ───")); - lines.push(""); - for (const tag of event.tags) { - lines.push(` ${tag.key}: ${tag.value}`); + // Tags + if (event.tags?.length) { + lines.push(""); + lines.push(muted("─── Tags ───")); + lines.push(""); + for (const tag of event.tags) { + lines.push(` ${tag.key}: ${tag.value}`); + } } - } - return lines; + return lines; + }); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 9ce0cf14..e7ee7592 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -13,10 +13,7 @@ import { } from "../types/index.js"; import { setAuthToken } from "./db/auth.js"; import { ApiError, AuthError, ConfigError, DeviceFlowError } from "./errors.js"; - -// ───────────────────────────────────────────────────────────────────────────── -// Configuration -// ───────────────────────────────────────────────────────────────────────────── +import { withHttpSpan } from "./telemetry.js"; // Sentry instance URL (supports self-hosted via env override) const SENTRY_URL = process.env.SENTRY_URL ?? "https://sentry.io"; @@ -45,10 +42,6 @@ const SCOPES = [ "team:read", ].join(" "); -// ───────────────────────────────────────────────────────────────────────────── -// Types -// ───────────────────────────────────────────────────────────────────────────── - type DeviceFlowCallbacks = { onUserCode: ( userCode: string, @@ -58,40 +51,20 @@ type DeviceFlowCallbacks = { onPolling?: () => void; }; -// ───────────────────────────────────────────────────────────────────────────── -// Utilities -// ───────────────────────────────────────────────────────────────────────────── - function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -// ───────────────────────────────────────────────────────────────────────────── -// Device Flow Implementation (RFC 8628) -// ───────────────────────────────────────────────────────────────────────────── - /** - * Request a device code from Sentry's device authorization endpoint + * Wrap a fetch call with connection error handling. + * Converts network errors into user-friendly ApiError messages. */ -async function requestDeviceCode() { - if (!SENTRY_CLIENT_ID) { - throw new ConfigError( - "SENTRY_CLIENT_ID is required for authentication", - "Set SENTRY_CLIENT_ID environment variable or use a pre-built binary" - ); - } - - let response: Response; - +async function fetchWithConnectionError( + url: string, + init: RequestInit +): Promise { try { - response = await fetch(`${SENTRY_URL}/oauth/device/code/`, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: SENTRY_CLIENT_ID, - scope: SCOPES, - }), - }); + return await fetch(url, init); } catch (error) { const isConnectionError = error instanceof Error && @@ -108,70 +81,96 @@ async function requestDeviceCode() { } throw error; } +} - if (!response.ok) { - const errorText = await response.text(); - throw new ApiError( - "Failed to initiate device flow", - response.status, - errorText, - "/oauth/device/code/" +/** Request a device code from Sentry's device authorization endpoint */ +function requestDeviceCode() { + if (!SENTRY_CLIENT_ID) { + throw new ConfigError( + "SENTRY_CLIENT_ID is required for authentication", + "Set SENTRY_CLIENT_ID environment variable or use a pre-built binary" ); } - const data = await response.json(); - - const result = DeviceCodeResponseSchema.safeParse(data); - if (!result.success) { - throw new ApiError( - "Invalid response from device authorization endpoint", - response.status, - result.error.errors.map((e) => e.message).join(", "), - "/oauth/device/code/" + return withHttpSpan("POST", "/oauth/device/code/", async () => { + const response = await fetchWithConnectionError( + `${SENTRY_URL}/oauth/device/code/`, + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: SENTRY_CLIENT_ID, + scope: SCOPES, + }), + } ); - } - return result.data; + if (!response.ok) { + const errorText = await response.text(); + throw new ApiError( + "Failed to initiate device flow", + response.status, + errorText, + "/oauth/device/code/" + ); + } + + const data = await response.json(); + + const result = DeviceCodeResponseSchema.safeParse(data); + if (!result.success) { + throw new ApiError( + "Invalid response from device authorization endpoint", + response.status, + result.error.errors.map((e) => e.message).join(", "), + "/oauth/device/code/" + ); + } + + return result.data; + }); } /** * Poll Sentry's token endpoint for the access token */ -async function pollForToken(deviceCode: string): Promise { - const response = await fetch(`${SENTRY_URL}/oauth/token/`, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: SENTRY_CLIENT_ID, - device_code: deviceCode, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }), - }); +function pollForToken(deviceCode: string): Promise { + return withHttpSpan("POST", "/oauth/token/", async () => { + const response = await fetch(`${SENTRY_URL}/oauth/token/`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: SENTRY_CLIENT_ID, + device_code: deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + }); - const data = await response.json(); + const data = await response.json(); - // Try to parse as success response first - const tokenResult = TokenResponseSchema.safeParse(data); - if (tokenResult.success) { - return tokenResult.data; - } + // Try to parse as success response first + const tokenResult = TokenResponseSchema.safeParse(data); + if (tokenResult.success) { + return tokenResult.data; + } - // Try to parse as error response - const errorResult = TokenErrorResponseSchema.safeParse(data); - if (errorResult.success) { - throw new DeviceFlowError( - errorResult.data.error, - errorResult.data.error_description - ); - } + // Try to parse as error response + const errorResult = TokenErrorResponseSchema.safeParse(data); + if (errorResult.success) { + throw new DeviceFlowError( + errorResult.data.error, + errorResult.data.error_description + ); + } - // If neither schema matches, throw a generic error - throw new ApiError( - "Unexpected response from token endpoint", - response.status, - JSON.stringify(data), - "/oauth/token/" - ); + // If neither schema matches, throw a generic error + throw new ApiError( + "Unexpected response from token endpoint", + response.status, + JSON.stringify(data), + "/oauth/token/" + ); + }); } type PollResult = @@ -281,10 +280,6 @@ export async function performDeviceFlow( ); } -// ───────────────────────────────────────────────────────────────────────────── -// Public API -// ───────────────────────────────────────────────────────────────────────────── - /** * Complete the OAuth flow by storing the token in the config file. * @@ -311,14 +306,8 @@ export async function setApiToken(token: string): Promise { await setAuthToken(token); } -// ───────────────────────────────────────────────────────────────────────────── -// Token Refresh (RFC 6749 Section 6) -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Refresh an access token using a refresh token. - */ -export async function refreshAccessToken( +/** Refresh an access token using a refresh token. */ +export function refreshAccessToken( refreshToken: string ): Promise { if (!SENTRY_CLIENT_ID) { @@ -328,65 +317,51 @@ export async function refreshAccessToken( ); } - let response: Response; + return withHttpSpan("POST", "/oauth/token/", async () => { + const response = await fetchWithConnectionError( + `${SENTRY_URL}/oauth/token/`, + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: SENTRY_CLIENT_ID, + grant_type: "refresh_token", + refresh_token: refreshToken, + }), + } + ); - try { - response = await fetch(`${SENTRY_URL}/oauth/token/`, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: SENTRY_CLIENT_ID, - grant_type: "refresh_token", - refresh_token: refreshToken, - }), - }); - } catch (error) { - const isConnectionError = - error instanceof Error && - (error.message.includes("ECONNREFUSED") || - error.message.includes("fetch failed") || - error.message.includes("network")); + if (!response.ok) { + let errorDetail = "Token refresh failed"; + try { + const errorData = await response.json(); + const errorResult = TokenErrorResponseSchema.safeParse(errorData); + if (errorResult.success) { + errorDetail = + errorResult.data.error_description ?? errorResult.data.error; + } + } catch { + // Ignore JSON parse errors + } - if (isConnectionError) { - throw new ApiError( - `Cannot connect to Sentry at ${SENTRY_URL}`, - 0, - "Check your network connection and SENTRY_URL configuration" + throw new AuthError( + "expired", + `Session expired: ${errorDetail}. Run 'sentry auth login' to re-authenticate.` ); } - throw error; - } - if (!response.ok) { - let errorDetail = "Token refresh failed"; - try { - const errorData = await response.json(); - const errorResult = TokenErrorResponseSchema.safeParse(errorData); - if (errorResult.success) { - errorDetail = - errorResult.data.error_description ?? errorResult.data.error; - } - } catch { - // Ignore JSON parse errors - } + const data = await response.json(); + const result = TokenResponseSchema.safeParse(data); - throw new AuthError( - "expired", - `Session expired: ${errorDetail}. Run 'sentry auth login' to re-authenticate.` - ); - } - - const data = await response.json(); - const result = TokenResponseSchema.safeParse(data); - - if (!result.success) { - throw new ApiError( - "Invalid response from token refresh endpoint", - response.status, - result.error.errors.map((e) => e.message).join(", "), - "/oauth/token/" - ); - } + if (!result.success) { + throw new ApiError( + "Invalid response from token refresh endpoint", + response.status, + result.error.errors.map((e) => e.message).join(", "), + "/oauth/token/" + ); + } - return result.data; + return result.data; + }); } diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index 24d02ca0..f7370353 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -10,9 +10,42 @@ */ // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import -import * as Sentry from "@sentry/node"; +import * as Sentry from "@sentry/bun"; import { CLI_VERSION, SENTRY_CLI_DSN } from "./constants.js"; +export type { Span } from "@sentry/bun"; + +/** Re-imported locally because Span is exported via re-export */ +type Span = Sentry.Span; + +/** + * Initialize telemetry context with user and instance information. + * Called after Sentry is initialized to set user context and instance tags. + */ +async function initTelemetryContext(): Promise { + try { + // Dynamic imports to avoid circular dependencies and for ES module compatibility + const { getUserInfo } = await import("./db/user.js"); + const { getInstanceId } = await import("./db/instance.js"); + + const user = getUserInfo(); + const instanceId = getInstanceId(); + + if (user) { + // Only send user ID - email/username are PII + Sentry.setUser({ id: user.userId }); + } + + if (instanceId) { + Sentry.setTag("instance_id", instanceId); + } + } catch (error) { + // Context initialization is not critical - continue without it + // But capture the error for debugging + Sentry.captureException(error); + } +} + /** * Wrap CLI execution with telemetry tracking. * @@ -20,25 +53,34 @@ import { CLI_VERSION, SENTRY_CLI_DSN } from "./constants.js"; * Captures any unhandled exceptions and reports them. * Telemetry can be disabled via SENTRY_CLI_NO_TELEMETRY=1 env var. * - * @param callback - The CLI execution function to wrap + * @param callback - The CLI execution function to wrap, receives the span for naming * @returns The result of the callback */ export async function withTelemetry( - callback: () => T | Promise + callback: (span: Span | undefined) => T | Promise ): Promise { const enabled = process.env.SENTRY_CLI_NO_TELEMETRY !== "1"; const client = initSentry(enabled); if (!client?.getOptions().enabled) { - return callback(); + return callback(undefined); } + // Initialize user and instance context + await initTelemetryContext(); + Sentry.startSession(); Sentry.captureSession(); try { - return await Sentry.startSpan( - { name: "cli-execution", op: "cli.command" }, - async () => callback() + return await Sentry.startSpanManual( + { name: "cli.command", op: "cli.command", forceTransaction: true }, + async (span) => { + try { + return await callback(span); + } finally { + span.end(); + } + } ); } catch (e) { Sentry.captureException(e); @@ -66,7 +108,7 @@ export async function withTelemetry( * * @internal Exported for testing */ -export function initSentry(enabled: boolean): Sentry.NodeClient | undefined { +export function initSentry(enabled: boolean): Sentry.BunClient | undefined { const environment = process.env.NODE_ENV ?? "development"; const client = Sentry.init({ @@ -124,21 +166,124 @@ export function initSentry(enabled: boolean): Sentry.NodeClient | undefined { } /** - * Set the command name for telemetry. + * Set the command name on the telemetry span. * * Called by stricli's forCommand context builder with the resolved * command path (e.g., "auth.login", "issue.list"). * - * Updates both the active span name and sets a tag for filtering. - * + * @param span - The span to update (from withTelemetry callback) * @param command - The command name (dot-separated path) */ -export function setCommandName(command: string): void { - // Update the span name to the actual command - const span = Sentry.getActiveSpan(); +export function setCommandSpanName( + span: Span | undefined, + command: string +): void { if (span) { - span.updateName(command); + Sentry.updateSpanName(span, command); } // Also set as tag for easier filtering in Sentry UI Sentry.setTag("command", command); } + +/** + * Set organization and project context as tags. + * + * Call this from commands after resolving the target org/project + * to enable filtering by org/project in Sentry. + * Accepts arrays to support multi-project commands. + * + * @param orgs - Organization slugs + * @param projects - Project slugs + */ +export function setOrgProjectContext(orgs: string[], projects: string[]): void { + if (orgs.length > 0) { + Sentry.setTag("sentry.org", orgs.join(",")); + } + if (projects.length > 0) { + Sentry.setTag("sentry.project", projects.join(",")); + } +} + +/** + * Wrap an HTTP request with a span for tracing. + * + * Creates a child span under the current active span to track + * HTTP request duration and status. + * + * @param method - HTTP method (GET, POST, etc.) + * @param url - Request URL or path + * @param fn - The async function that performs the HTTP request + * @returns The result of the function + */ +export function withHttpSpan( + method: string, + url: string, + fn: () => Promise +): Promise { + return Sentry.startSpan( + { + name: `${method} ${url}`, + op: "http.client", + attributes: { + "http.request.method": method, + "url.path": url, + }, + onlyIfParent: true, + }, + async (span) => { + try { + const result = await fn(); + span.setStatus({ code: 1 }); // OK + return result; + } catch (error) { + span.setStatus({ code: 2 }); // Error + throw error; + } + } + ); +} + +/** + * Wrap a database operation with a span for tracing. + * + * Creates a child span under the current active span to track + * database operation duration. + * + * @param operation - Name of the operation (e.g., "getAuthToken", "setDefaults") + * @param fn - The function that performs the database operation + * @returns The result of the function + */ +export function withDbSpan(operation: string, fn: () => T): T { + return Sentry.startSpan( + { + name: operation, + op: "db", + attributes: { + "db.system": "sqlite", + }, + onlyIfParent: true, + }, + fn + ); +} + +/** + * Wrap a serialization/formatting operation with a span for tracing. + * + * Creates a child span under the current active span to track + * expensive formatting operations. + * + * @param operation - Name of the operation (e.g., "formatSpanTree") + * @param fn - The function that performs the formatting + * @returns The result of the function + */ +export function withSerializeSpan(operation: string, fn: () => T): T { + return Sentry.startSpan( + { + name: operation, + op: "serialize", + onlyIfParent: true, + }, + fn + ); +} diff --git a/src/types/index.ts b/src/types/index.ts index 81e7bfa3..3e487ada 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -71,6 +71,8 @@ export type { // Organization & Project SentryOrganization, SentryProject, + // User + SentryUser, // Span/Trace types Span, StackFrame, @@ -102,6 +104,7 @@ export { SentryIssueSchema, SentryOrganizationSchema, SentryProjectSchema, + SentryUserSchema, SpanSchema, StackFrameSchema, StacktraceSchema, diff --git a/src/types/sentry.ts b/src/types/sentry.ts index 20f714de..8a80de31 100644 --- a/src/types/sentry.ts +++ b/src/types/sentry.ts @@ -35,6 +35,23 @@ export const SentryOrganizationSchema = z export type SentryOrganization = z.infer; +// ───────────────────────────────────────────────────────────────────────────── +// User +// ───────────────────────────────────────────────────────────────────────────── + +export const SentryUserSchema = z + .object({ + // Core identifiers (required) + id: z.string(), + // Optional user info + email: z.string().optional(), + username: z.string().optional(), + name: z.string().optional(), + }) + .passthrough(); + +export type SentryUser = z.infer; + // ───────────────────────────────────────────────────────────────────────────── // Project // ───────────────────────────────────────────────────────────────────────────── diff --git a/test/fixtures/user.json b/test/fixtures/user.json new file mode 100644 index 00000000..60707238 --- /dev/null +++ b/test/fixtures/user.json @@ -0,0 +1,6 @@ +{ + "id": "12345", + "email": "test@example.com", + "username": "testuser", + "name": "Test User" +} diff --git a/test/lib/db/user.test.ts b/test/lib/db/user.test.ts new file mode 100644 index 00000000..2330f21b --- /dev/null +++ b/test/lib/db/user.test.ts @@ -0,0 +1,85 @@ +/** + * User Info Storage Tests + */ + +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { getUserInfo, setUserInfo } from "../../../src/lib/db/user.js"; +import { cleanupTestDir, createTestConfigDir } from "../../helpers.js"; + +let testConfigDir: string; + +beforeEach(async () => { + testConfigDir = await createTestConfigDir("test-user-"); + process.env.SENTRY_CLI_CONFIG_DIR = testConfigDir; +}); + +afterEach(async () => { + delete process.env.SENTRY_CLI_CONFIG_DIR; + await cleanupTestDir(testConfigDir); +}); + +describe("getUserInfo", () => { + test("returns undefined when no user info stored", () => { + const result = getUserInfo(); + expect(result).toBeUndefined(); + }); + + test("returns stored user info", () => { + setUserInfo({ + userId: "12345", + email: "test@example.com", + username: "testuser", + }); + + const result = getUserInfo(); + expect(result).toEqual({ + userId: "12345", + email: "test@example.com", + username: "testuser", + }); + }); + + test("handles missing email and username", () => { + setUserInfo({ userId: "12345" }); + + const result = getUserInfo(); + expect(result).toEqual({ + userId: "12345", + email: undefined, + username: undefined, + }); + }); +}); + +describe("setUserInfo", () => { + test("stores user info with all fields", () => { + setUserInfo({ + userId: "user123", + email: "user@test.com", + username: "myuser", + }); + + const result = getUserInfo(); + expect(result?.userId).toBe("user123"); + expect(result?.email).toBe("user@test.com"); + expect(result?.username).toBe("myuser"); + }); + + test("overwrites existing user info", () => { + setUserInfo({ userId: "first", email: "first@test.com" }); + setUserInfo({ userId: "second", email: "second@test.com" }); + + const result = getUserInfo(); + expect(result?.userId).toBe("second"); + expect(result?.email).toBe("second@test.com"); + }); + + test("stores user info with only userId", () => { + setUserInfo({ userId: "minimal" }); + + const result = getUserInfo(); + expect(result?.userId).toBe("minimal"); + expect(result?.email).toBeUndefined(); + expect(result?.username).toBeUndefined(); + }); +}); diff --git a/test/lib/telemetry.test.ts b/test/lib/telemetry.test.ts index 062aedf0..50c47e18 100644 --- a/test/lib/telemetry.test.ts +++ b/test/lib/telemetry.test.ts @@ -5,7 +5,15 @@ */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { initSentry, withTelemetry } from "../../src/lib/telemetry.js"; +import { + initSentry, + setCommandSpanName, + setOrgProjectContext, + withDbSpan, + withHttpSpan, + withSerializeSpan, + withTelemetry, +} from "../../src/lib/telemetry.js"; describe("initSentry", () => { test("returns client with enabled=false when disabled", () => { @@ -110,3 +118,73 @@ describe("withTelemetry", () => { expect(executed).toBe(true); }); }); + +describe("setCommandSpanName", () => { + test("handles undefined span gracefully", () => { + // Should not throw when span is undefined + expect(() => setCommandSpanName(undefined, "test.command")).not.toThrow(); + }); +}); + +describe("setOrgProjectContext", () => { + test("handles empty arrays", () => { + expect(() => setOrgProjectContext([], [])).not.toThrow(); + }); + + test("handles single org/project", () => { + expect(() => + setOrgProjectContext(["my-org"], ["my-project"]) + ).not.toThrow(); + }); + + test("handles multiple orgs/projects", () => { + expect(() => + setOrgProjectContext(["org1", "org2"], ["proj1", "proj2"]) + ).not.toThrow(); + }); +}); + +describe("withHttpSpan", () => { + test("executes function and returns result", async () => { + const result = await withHttpSpan("GET", "/test", async () => "success"); + expect(result).toBe("success"); + }); + + test("propagates errors", async () => { + await expect( + withHttpSpan("POST", "/test", async () => { + throw new Error("http error"); + }) + ).rejects.toThrow("http error"); + }); +}); + +describe("withDbSpan", () => { + test("executes function and returns result", () => { + const result = withDbSpan("testOp", () => 42); + expect(result).toBe(42); + }); + + test("propagates errors", () => { + expect(() => + withDbSpan("testOp", () => { + throw new Error("db error"); + }) + ).toThrow("db error"); + }); +}); + +describe("withSerializeSpan", () => { + test("executes function and returns result", () => { + const result = withSerializeSpan("format", () => ({ formatted: true })); + expect(result).toEqual({ formatted: true }); + }); + + test("propagates errors", () => { + expect(() => + withSerializeSpan("format", () => { + throw new Error("serialize error"); + }) + ).toThrow("serialize error"); + }); +}); diff --git a/test/mocks/routes.ts b/test/mocks/routes.ts index 513214e7..b29157fa 100644 --- a/test/mocks/routes.ts +++ b/test/mocks/routes.ts @@ -14,6 +14,7 @@ import organizationFixture from "../fixtures/organization.json"; import organizationsFixture from "../fixtures/organizations.json"; import projectFixture from "../fixtures/project.json"; import projectsFixture from "../fixtures/projects.json"; +import userFixture from "../fixtures/user.json"; import type { MockRoute, MockServer } from "./server.js"; import { createMockServer } from "./server.js"; @@ -25,6 +26,13 @@ export const TEST_ISSUE_SHORT_ID = "TEST-PROJECT-1A"; export const TEST_EVENT_ID = "abc123def456abc123def456abc12345"; export const apiRoutes: MockRoute[] = [ + // Users + { + method: "GET", + path: "/api/0/users/me/", + response: userFixture, + }, + // Organizations { method: "GET",