diff --git a/package-lock.json b/package-lock.json index 885a47f5..75859e19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,7 @@ "compare-versions": "^4.1.4", "dotenv": "^16.4.5", "lodash": "^4.18.0", - "uuid": "^8.3.2", - "vscode-extension-telemetry-wrapper": "^0.14.0", + "vscode-extension-telemetry-wrapper": "^0.15.2", "vscode-languageclient": "6.0.0-next.9", "vscode-languageserver-types": "3.16.0", "vscode-tas-client": "^0.1.84" @@ -115,68 +114,72 @@ } }, "node_modules/@microsoft/1ds-core-js": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", - "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.4.1.tgz", + "integrity": "sha512-utqwacfUkiGJROn4WC7aNdRBsRxwhNWXuqaJM2B0N0WHmv1+IhSuI7RQ3FHwxRP1dxZi/xn9aELMZ7HMStsW1w==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "node_modules/@microsoft/1ds-post-js": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", - "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.4.1.tgz", + "integrity": "sha512-CkFEhDY7X8E2JLr6HsEvRiC0DaLOCsA7vlbq/9DJP65gAumgw2NnFNIAOg6Je5Geq1LDu76/nb2hP34p8eGggw==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "4.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "node_modules/@microsoft/applicationinsights-channel-js": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", - "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.4.1.tgz", + "integrity": "sha512-QS1k6iwVwR1MznGAB1H0F9raqpevbFNbadLS5O1419pz9OEWBfF9wRQLnENCyo8QS9Q0IdiqnGAON/D8IywpWg==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-common": "3.3.4", - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, "peerDependencies": { "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-common": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", - "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.4.1.tgz", + "integrity": "sha512-CTbD0g/68tiv2yCItsodDQBYxyHdfQkG7VhvVU8OHenukpl/7W4wEuxZuOntqhv5m9Nx/DFncbz+T83nvYTG3g==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, "peerDependencies": { "tslib": ">= 1.0.0" } }, "node_modules/@microsoft/applicationinsights-core-js": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", - "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.4.1.tgz", + "integrity": "sha512-eXIHZ1+nvBiJgVpufBiTP801Vtr5FEwjWZioUsb44NC/z/UcsZh2MDJ1mBpjaDO73LVYUw/ZZmDCCo6Pg/61kA==", + "license": "MIT", "dependencies": { "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, "peerDependencies": { "tslib": ">= 1.0.0" @@ -186,22 +189,23 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, "node_modules/@microsoft/applicationinsights-web-basic": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", - "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.4.1.tgz", + "integrity": "sha512-V/hSlauFp1thJa57+TMv5mAYinJAQUi4zOmDmpahnDgs8g1zrQ0D8QYDmu0Zfi+9GhoD80B4yJez2+ydJPJz2w==", + "license": "MIT", "dependencies": { - "@microsoft/applicationinsights-channel-js": "3.3.4", - "@microsoft/applicationinsights-common": "3.3.4", - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-channel-js": "3.4.1", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, "peerDependencies": { "tslib": ">= 1.0.0" @@ -211,22 +215,34 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", "dependencies": { "@nevware21/ts-utils": ">= 0.10.4 < 2.x" } }, "node_modules/@nevware21/ts-async": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.3.tgz", - "integrity": "sha512-UsF7eerLsVfid7iV1oXF80qXBwHNBeqSqfh/nPZgirRU1MACmSsj83EZKS2ViFHVfSGG6WIuXMGBP6KciXfYhA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.5.tgz", + "integrity": "sha512-vwqaL05iJPjLeh5igPi8MeeAu10i+Aq7xko1fbo9F5Si6MnVN5505qaV7AhSdk5MCBJVT/UYMk3kgInNjDb4Ig==", + "license": "MIT", "dependencies": { - "@nevware21/ts-utils": ">= 0.11.5 < 2.x" + "@nevware21/ts-utils": ">= 0.12.2 < 2.x" } }, "node_modules/@nevware21/ts-utils": { - "version": "0.11.5", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.5.tgz", - "integrity": "sha512-7nIzWKR50mf3htOg53kwPLqD5iJaRfVyBvb1NJhlIncyP1WzK8vAQbU9rqIsRtv7td1CnqspdP6IWNEjOjaeug==" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.14.0.tgz", + "integrity": "sha512-WoeqTIXQ8WPhl+lD2NbMHoAQ4sJl0n7EoRoDmVJui//Usg512enl9q1fdbVobuZt3omnxnmVsDrNIvPBvFgddQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nevware21" + }, + { + "type": "other", + "url": "https://buymeacoffee.com/nevware21" + } + ] }, "node_modules/@types/eslint": { "version": "9.6.1", @@ -308,13 +324,14 @@ "license": "MIT" }, "node_modules/@vscode/extension-telemetry": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.7.tgz", - "integrity": "sha512-2GQbcfDUTg0QC1v0HefkHNwYrE5LYKzS3Zb0+uA6Qn1MBDzgiSh23ddOZF/JRqhqBFOG0mE70XslKSGQ5v9KwQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-1.5.1.tgz", + "integrity": "sha512-rnRRQIRCwRdbcQ0QV5ajKJRz8noEIoQA2hX9VjAlVAVB85+ClbaPNhljobFXgW31ue69FRO6KPE4XJ/lLgKt/Q==", + "license": "MIT", "dependencies": { - "@microsoft/1ds-core-js": "^4.3.0", - "@microsoft/1ds-post-js": "^4.3.0", - "@microsoft/applicationinsights-web-basic": "^3.3.0" + "@microsoft/1ds-core-js": "^4.3.10", + "@microsoft/1ds-post-js": "^4.3.10", + "@microsoft/applicationinsights-web-basic": "^3.3.10" }, "engines": { "vscode": "^1.75.0" @@ -1275,9 +1292,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -2883,21 +2900,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vscode-extension-telemetry-wrapper": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/vscode-extension-telemetry-wrapper/-/vscode-extension-telemetry-wrapper-0.14.0.tgz", - "integrity": "sha512-EYr1hqiYVSGfupchDN405zSwuvA8V3tJ62KcLIRDr/4ongOc2AvSZ0BlRq8a0w950tadsMlXTKEheB97fZBttg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/vscode-extension-telemetry-wrapper/-/vscode-extension-telemetry-wrapper-0.15.2.tgz", + "integrity": "sha512-efKkHF8c4kTKyBhBH2k0bZU4drqIic2jBYw/j1ixKOEEsa/WIiuUsdrBPD5uaRIoZ/91GzNCLiiV4ckIrf581g==", + "license": "MIT", "dependencies": { - "@vscode/extension-telemetry": "^0.9.6", - "uuid": "^8.3.2" + "@microsoft/applicationinsights-common": "^3.4.1", + "@vscode/extension-telemetry": "^1.2.0" } }, "node_modules/vscode-jsonrpc": { @@ -3361,62 +3371,61 @@ } }, "@microsoft/1ds-core-js": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", - "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.4.1.tgz", + "integrity": "sha512-utqwacfUkiGJROn4WC7aNdRBsRxwhNWXuqaJM2B0N0WHmv1+IhSuI7RQ3FHwxRP1dxZi/xn9aELMZ7HMStsW1w==", "requires": { - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "@microsoft/1ds-post-js": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", - "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.4.1.tgz", + "integrity": "sha512-CkFEhDY7X8E2JLr6HsEvRiC0DaLOCsA7vlbq/9DJP65gAumgw2NnFNIAOg6Je5Geq1LDu76/nb2hP34p8eGggw==", "requires": { - "@microsoft/1ds-core-js": "4.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "@microsoft/applicationinsights-channel-js": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", - "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.4.1.tgz", + "integrity": "sha512-QS1k6iwVwR1MznGAB1H0F9raqpevbFNbadLS5O1419pz9OEWBfF9wRQLnENCyo8QS9Q0IdiqnGAON/D8IywpWg==", "requires": { - "@microsoft/applicationinsights-common": "3.3.4", - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "@microsoft/applicationinsights-common": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", - "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.4.1.tgz", + "integrity": "sha512-CTbD0g/68tiv2yCItsodDQBYxyHdfQkG7VhvVU8OHenukpl/7W4wEuxZuOntqhv5m9Nx/DFncbz+T83nvYTG3g==", "requires": { - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "@microsoft/applicationinsights-core-js": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", - "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.4.1.tgz", + "integrity": "sha512-eXIHZ1+nvBiJgVpufBiTP801Vtr5FEwjWZioUsb44NC/z/UcsZh2MDJ1mBpjaDO73LVYUw/ZZmDCCo6Pg/61kA==", "requires": { "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "@microsoft/applicationinsights-shims": { @@ -3428,17 +3437,16 @@ } }, "@microsoft/applicationinsights-web-basic": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", - "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.4.1.tgz", + "integrity": "sha512-V/hSlauFp1thJa57+TMv5mAYinJAQUi4zOmDmpahnDgs8g1zrQ0D8QYDmu0Zfi+9GhoD80B4yJez2+ydJPJz2w==", "requires": { - "@microsoft/applicationinsights-channel-js": "3.3.4", - "@microsoft/applicationinsights-common": "3.3.4", - "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-channel-js": "3.4.1", + "@microsoft/applicationinsights-core-js": "3.4.1", "@microsoft/applicationinsights-shims": "3.0.1", "@microsoft/dynamicproto-js": "^2.0.3", - "@nevware21/ts-async": ">= 0.5.2 < 2.x", - "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" } }, "@microsoft/dynamicproto-js": { @@ -3450,17 +3458,17 @@ } }, "@nevware21/ts-async": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.3.tgz", - "integrity": "sha512-UsF7eerLsVfid7iV1oXF80qXBwHNBeqSqfh/nPZgirRU1MACmSsj83EZKS2ViFHVfSGG6WIuXMGBP6KciXfYhA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.5.tgz", + "integrity": "sha512-vwqaL05iJPjLeh5igPi8MeeAu10i+Aq7xko1fbo9F5Si6MnVN5505qaV7AhSdk5MCBJVT/UYMk3kgInNjDb4Ig==", "requires": { - "@nevware21/ts-utils": ">= 0.11.5 < 2.x" + "@nevware21/ts-utils": ">= 0.12.2 < 2.x" } }, "@nevware21/ts-utils": { - "version": "0.11.5", - "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.5.tgz", - "integrity": "sha512-7nIzWKR50mf3htOg53kwPLqD5iJaRfVyBvb1NJhlIncyP1WzK8vAQbU9rqIsRtv7td1CnqspdP6IWNEjOjaeug==" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.14.0.tgz", + "integrity": "sha512-WoeqTIXQ8WPhl+lD2NbMHoAQ4sJl0n7EoRoDmVJui//Usg512enl9q1fdbVobuZt3omnxnmVsDrNIvPBvFgddQ==" }, "@types/eslint": { "version": "9.6.1", @@ -3541,13 +3549,13 @@ "dev": true }, "@vscode/extension-telemetry": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.7.tgz", - "integrity": "sha512-2GQbcfDUTg0QC1v0HefkHNwYrE5LYKzS3Zb0+uA6Qn1MBDzgiSh23ddOZF/JRqhqBFOG0mE70XslKSGQ5v9KwQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-1.5.1.tgz", + "integrity": "sha512-rnRRQIRCwRdbcQ0QV5ajKJRz8noEIoQA2hX9VjAlVAVB85+ClbaPNhljobFXgW31ue69FRO6KPE4XJ/lLgKt/Q==", "requires": { - "@microsoft/1ds-core-js": "^4.3.0", - "@microsoft/1ds-post-js": "^4.3.0", - "@microsoft/applicationinsights-web-basic": "^3.3.0" + "@microsoft/1ds-core-js": "^4.3.10", + "@microsoft/1ds-post-js": "^4.3.10", + "@microsoft/applicationinsights-web-basic": "^3.3.10" } }, "@vscode/test-electron": { @@ -4251,9 +4259,9 @@ "dev": true }, "fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true }, "fastest-levenshtein": { @@ -5381,18 +5389,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "vscode-extension-telemetry-wrapper": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/vscode-extension-telemetry-wrapper/-/vscode-extension-telemetry-wrapper-0.14.0.tgz", - "integrity": "sha512-EYr1hqiYVSGfupchDN405zSwuvA8V3tJ62KcLIRDr/4ongOc2AvSZ0BlRq8a0w950tadsMlXTKEheB97fZBttg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/vscode-extension-telemetry-wrapper/-/vscode-extension-telemetry-wrapper-0.15.2.tgz", + "integrity": "sha512-efKkHF8c4kTKyBhBH2k0bZU4drqIic2jBYw/j1ixKOEEsa/WIiuUsdrBPD5uaRIoZ/91GzNCLiiV4ckIrf581g==", "requires": { - "@vscode/extension-telemetry": "^0.9.6", - "uuid": "^8.3.2" + "@microsoft/applicationinsights-common": "^3.4.1", + "@vscode/extension-telemetry": "^1.2.0" } }, "vscode-jsonrpc": { diff --git a/package.json b/package.json index 4a5650c4..cba4a021 100644 --- a/package.json +++ b/package.json @@ -1011,6 +1011,7 @@ ], "icon": "$(debug-alt)", "canBeReferencedInPrompt": true, + "when": "javaLSReady", "inputSchema": { "type": "object", "properties": { @@ -1062,6 +1063,7 @@ ], "icon": "$(debug-breakpoint)", "canBeReferencedInPrompt": true, + "when": "javaLSReady", "inputSchema": { "type": "object", "properties": { @@ -1105,6 +1107,7 @@ ], "icon": "$(debug-step-over)", "canBeReferencedInPrompt": true, + "when": "javaLSReady && inDebugMode && debugType == 'java'", "inputSchema": { "type": "object", "properties": { @@ -1142,6 +1145,7 @@ ], "icon": "$(symbol-variable)", "canBeReferencedInPrompt": true, + "when": "javaLSReady && inDebugMode && debugType == 'java'", "inputSchema": { "type": "object", "properties": { @@ -1183,6 +1187,7 @@ ], "icon": "$(call-hierarchy)", "canBeReferencedInPrompt": true, + "when": "javaLSReady && inDebugMode && debugType == 'java'", "inputSchema": { "type": "object", "properties": { @@ -1212,6 +1217,7 @@ ], "icon": "$(symbol-method)", "canBeReferencedInPrompt": true, + "when": "javaLSReady && inDebugMode && debugType == 'java'", "inputSchema": { "type": "object", "properties": { @@ -1257,6 +1263,7 @@ ], "icon": "$(list-tree)", "canBeReferencedInPrompt": true, + "when": "javaLSReady && inDebugMode && debugType == 'java'", "inputSchema": { "type": "object", "properties": {}, @@ -1275,6 +1282,7 @@ ], "icon": "$(debug-breakpoint-unverified)", "canBeReferencedInPrompt": true, + "when": "javaLSReady", "inputSchema": { "type": "object", "properties": { @@ -1303,6 +1311,7 @@ ], "icon": "$(debug-stop)", "canBeReferencedInPrompt": true, + "when": "javaLSReady && inDebugMode && debugType == 'java'", "inputSchema": { "type": "object", "properties": { @@ -1330,12 +1339,29 @@ ], "icon": "$(info)", "canBeReferencedInPrompt": true, + "when": "javaLSReady", "inputSchema": { "type": "object", "properties": {}, "required": [] } } + ], + "chatSkills": [ + { + "path": "./resources/skills/java-launch-troubleshooting/SKILL.md", + "when": "javaLSReady" + }, + { + "path": "./resources/skills/java-debug-inspection/SKILL.md", + "when": "javaLSReady && inDebugMode && debugType == 'java'" + } + ], + "chatInstructions": [ + { + "path": "./resources/instruments/javaDebugContext.instructions.md", + "when": "javaLSReady" + } ] }, "scripts": { @@ -1366,8 +1392,7 @@ "compare-versions": "^4.1.4", "dotenv": "^16.4.5", "lodash": "^4.18.0", - "uuid": "^8.3.2", - "vscode-extension-telemetry-wrapper": "^0.14.0", + "vscode-extension-telemetry-wrapper": "^0.15.2", "vscode-languageclient": "6.0.0-next.9", "vscode-languageserver-types": "3.16.0", "vscode-tas-client": "^0.1.84" diff --git a/resources/instruments/javaDebugContext.instructions.md b/resources/instruments/javaDebugContext.instructions.md new file mode 100644 index 00000000..055b01b0 --- /dev/null +++ b/resources/instruments/javaDebugContext.instructions.md @@ -0,0 +1,16 @@ +--- +description: Use when the user wants to run, launch, start, debug, verify, or step through a Java program (main class, Spring Boot, JAR, Maven, Gradle); or when inspecting variables, stack frames, threads, evaluating expressions, or managing breakpoints in a Java debug session. +--- + +For Java run/launch/debug/inspection requests, prefer the Java debug language model tools over generic shell commands (`mvn exec:java`, `gradle run`, raw `java -cp …`). These tools are contributed by the `Debugger for Java` extension and are deferred — activate them with `tool_search_tool_regex` using pattern `java_breakpoint|debug` before first use, then call them by name. + +## Pick the right skill + +| User intent | Load skill | Typical tools | +|---|---|---| +| Run / launch / start / stop a Java program, diagnose launch failures (build error, classpath, mainClass missing) | `java-launch-troubleshooting` | `debug_java_application`, `get_debug_session_info`, `stop_debug_session` | +| Inspect a Java program that is already being debugged: read variables, evaluate expressions, walk the stack, step in/over/out, continue, set or remove breakpoints | `java-debug-inspection` | `get_debug_variables`, `get_debug_stack_trace`, `evaluate_debug_expression`, `get_debug_threads`, `debug_step_operation`, `set_java_breakpoint`, `remove_java_breakpoints` | + +If both apply (e.g. "launch and break on entry of `Main.foo`"), load `java-launch-troubleshooting` first, then `java-debug-inspection` after the session is active. + +Fall back to `run_in_terminal` only when `debug_java_application` returns "Java Language Server not ready" or "project not detected". diff --git a/resources/skills/java-debug-inspection/SKILL.md b/resources/skills/java-debug-inspection/SKILL.md new file mode 100644 index 00000000..354132dd --- /dev/null +++ b/resources/skills/java-debug-inspection/SKILL.md @@ -0,0 +1,58 @@ +--- +name: java-debug-inspection +description: Use when inspecting a Java program that is already being debugged — read local variables, walk the call stack, list threads, evaluate expressions, step in/over/out, continue execution, or set / remove breakpoints in an active Java debug session. NOT for starting, launching, or stopping a debug session — use `java-launch-troubleshooting` for that. +--- + +# Java Debug Inspection + +Use this skill when a Java debug session is already active (the user has launched the program with `debug_java_application` or via the VS Code Run/Debug UI) and they want to **observe or steer the running program**. Typical user phrases: + +- "what's the value of `user.id` right now?", "show me the local variables" +- "evaluate `list.size()`", "what does `service.findById(42)` return at this frame?" +- "show the stack trace", "what threads are running?", "go up one frame" +- "step in", "step over", "step out", "continue", "resume" +- "set a breakpoint at `OrderService.placeOrder`", "remove the breakpoint on line 42" + +If no debug session is active, this skill should not be used — load `java-launch-troubleshooting` first to start one. + +## Tools + +These language model tools are contributed by the `Debugger for Java` extension and are deferred. Activate them with `tool_search_tool_regex` using pattern `java_breakpoint|debug` before first use. + +| Tool | Purpose | +|---|---| +| `get_debug_variables` | Read local variables, fields, and watched expressions in the current frame. | +| `get_debug_stack_trace` | List call-stack frames for the focused (or specified) thread. | +| `get_debug_threads` | List all threads in the session with state (running, stopped, terminated). | +| `evaluate_debug_expression` | Evaluate an arbitrary Java expression in the context of the focused frame. | +| `debug_step_operation` | Step in, step over, step out, or continue. Requires the program to be paused. | +| `set_java_breakpoint` | Set a line / method / exception breakpoint. Works at any time during the session. | +| `remove_java_breakpoints` | Remove one or more breakpoints by ID or location. | + +## Preferred Workflow + +1. **Confirm session state.** If the user asks to step or evaluate an expression, the program must be paused (typically at a breakpoint). If unsure, call `get_debug_session_info` from `java-launch-troubleshooting` to check, or list threads with `get_debug_threads`. +2. **Inspect first, then act.** For "what's the value of X" — call `get_debug_variables` or `evaluate_debug_expression`. Do not guess from source code. +3. **Step / continue.** For "step over" / "step in" / "step out" / "continue", call `debug_step_operation` with the matching action. After each step, re-read variables or stack as needed. +4. **Breakpoints.** For "set a breakpoint at …", call `set_java_breakpoint` with the file URI + line, or fully qualified method signature, or exception class. For "remove the breakpoint at …", call `remove_java_breakpoints`. +5. **Report precisely.** Quote the exact value or stack frame returned by the tool — do not paraphrase. + +## Common Pitfalls + +| Symptom | Likely cause | Fix | +|---|---|---| +| `debug_step_operation` returns "thread is not paused" | The program is running, not stopped at a breakpoint | Ask the user to add a breakpoint first, or wait for the next stop event | +| `evaluate_debug_expression` returns `` | No focused stack frame, often because the session just resumed | Re-call `get_debug_stack_trace` to refocus a frame | +| `get_debug_variables` returns empty for a parameter | Compilation without `-g` (no local variable table) | Inform user; offer to inspect via `evaluate_debug_expression` instead | +| Step in jumps into JDK internals | Default step filters disabled | Suggest enabling `java.debug.settings.stepping.skipClasses` | + +## When NOT to Use This Skill + +- The user wants to *start* a Java program (no session yet) → use `java-launch-troubleshooting` +- The user wants to *stop* the session → use `stop_debug_session` from `java-launch-troubleshooting` +- The program is a non-Java language → do not load this skill +- The user is editing source code without an active debug session → do nothing + +## Fallback + +If a tool returns "Java Language Server not ready" or repeats the same error twice, report the raw error to the user and stop calling debug tools for the current turn. Do not retry more than twice. diff --git a/resources/skills/java-launch-troubleshooting/SKILL.md b/resources/skills/java-launch-troubleshooting/SKILL.md new file mode 100644 index 00000000..c14dc527 --- /dev/null +++ b/resources/skills/java-launch-troubleshooting/SKILL.md @@ -0,0 +1,50 @@ +--- +name: java-launch-troubleshooting +description: Use when the user wants to run, launch, start, restart, or stop a Java program (main class, Spring Boot, JAR, Maven, Gradle), or diagnose launch failures (mainClass missing, classpath unresolved, compile failure, "project not detected", `ClassNotFoundException` at startup). NOT for inspecting variables, stepping, or setting breakpoints in an already-running debug session — use `java-debug-inspection` for that. +--- + +# Java Launch Troubleshooting + +Use this skill when the user wants to **start or stop** a Java program, or when an attempted launch fails. Typical user phrases: + +- "run this main class", "start the app", "launch the Spring Boot project", "run the jar" +- "stop the debug session", "kill the running app" +- prior `run_in_terminal` failed with `ClassNotFoundException`, `mainClass is not set`, `Could not find or load main class`, `Could not resolve classpath` +- the user changed `pom.xml` / `build.gradle` and the app no longer starts + +## Tools + +These language model tools are contributed by the `Debugger for Java` extension and are deferred. Activate them with `tool_search_tool_regex` using pattern `java_breakpoint|debug` before first use. + +| Tool | Purpose | +|---|---| +| `debug_java_application` | Build + resolve classpath + start JVM. Returns precise compile and classpath errors. | +| `get_debug_session_info` | Check whether a debug session is already running and its status. | +| `stop_debug_session` | Stop a running Java debug session cleanly. | + +## Preferred Workflow + +1. **Confirm intent.** Is the user trying to *run / start / launch / stop* a Java program (use this skill) or just edit code (do not load this skill)? +2. **Check existing session.** Call `get_debug_session_info` first. If a session is already running for the target, do not launch a second one. +3. **Launch.** Call `debug_java_application` with `target` = the fully qualified main class or JAR, and `workspacePath` = the project root containing `pom.xml`, `build.gradle`, or `.classpath`. Let `skipBuild` default to `false` so the tool handles compilation. +4. **Read the error.** If `debug_java_application` fails, the error message is structured (mainClass missing, classpath unresolved, build failure with line number). Use it to suggest a fix — do not retry with `run_in_terminal`. +5. **Stop when done.** When the user says "stop", "kill it", or has the answer they need, call `stop_debug_session`. + +## Common Failure Modes + +| Symptom from `debug_java_application` | Likely cause | Suggested fix | +|---|---|---| +| `mainClass is not configured` / `mainClass missing` | Project has no `launch.json`, and the file has no `public static void main` | Ask user which class to launch, or generate `launch.json` | +| `Could not resolve classpath` | Maven/Gradle import has not completed, or `pom.xml` has unresolved dependencies | Wait for Java Language Server import, then ask user to run `Java: Clean Java Language Server Workspace` | +| `Compilation failed` with file:line | Source code has a compile error | Fix the reported error in the source file, do not retry the launch | +| `Project not detected` | `workspacePath` does not contain a build file | Re-check `workspacePath`; for multi-module projects, use the module root, not the repo root | + +## When NOT to Use This Skill + +- The user is editing or refactoring Java code without running it → do nothing +- The user is already inside a live debug session and wants to inspect variables, evaluate expressions, walk the stack, step, or set / remove breakpoints → use `java-debug-inspection` instead, do not re-launch +- The program is a non-Java language → do not load this skill + +## Fallback + +If `debug_java_application` returns `Java Language Server not ready` or repeats the same error twice, fall back to `run_in_terminal` with the appropriate `mvn` or `gradle` command and report the raw output to the user. Do not retry the debug tool more than twice. diff --git a/src/extension.ts b/src/extension.ts index e3a75eb0..36025559 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,7 @@ import { JavaDebugAdapterDescriptorFactory } from "./javaDebugAdapterDescriptorF import { JavaInlineValuesProvider } from "./JavaInlineValueProvider"; import { logJavaException, logJavaInfo } from "./javaLogger"; import { registerLanguageModelTool, registerDebugSessionTools } from "./languageModelTool"; +import { recordChatActivation } from "./lmToolTelemetry"; import { IMainClassOption, IMainMethod, resolveMainMethod } from "./languageServerPlugin"; import { mainClassPicker } from "./mainClassPicker"; import { pickJavaProcess } from "./processPicker"; @@ -124,6 +125,29 @@ async function registerLanguageModelToolsWhenReady(context: vscode.ExtensionCont registerLanguageModelTool(context); const debugToolsDisposables = registerDebugSessionTools(context); context.subscriptions.push(...debugToolsDisposables); + + // One-shot activation snapshot so we can track coverage of the new chat surfaces over time. + // Counts only — no user data, no file paths, no class names. + try { + const pkg = context.extension?.packageJSON as { + version?: string; + contributes?: { + languageModelTools?: unknown[]; + chatSkills?: unknown[]; + chatInstructions?: unknown[]; + }; + } | undefined; + const contrib = pkg?.contributes ?? {}; + recordChatActivation({ + javaLSReadyAtActivation: !!javaExt.isActive, + lmtCount: contrib.languageModelTools?.length ?? 0, + chatSkillsCount: contrib.chatSkills?.length ?? 0, + chatInstructionsCount: contrib.chatInstructions?.length ?? 0, + extensionVersion: pkg?.version ?? "unknown", + }); + } catch { + // Telemetry must never break activation. + } } async function subscribeToJavaExtensionEvents(): Promise { diff --git a/src/languageModelTool.ts b/src/languageModelTool.ts index ad803d25..85b28855 100644 --- a/src/languageModelTool.ts +++ b/src/languageModelTool.ts @@ -4,7 +4,20 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { sendError, sendInfo } from "vscode-extension-telemetry-wrapper"; +import { + classifyBreakpoint, + classifyError, + classifyEvalContext, + classifyRemoveScope, + classifyScopeType, + classifyStep, + classifyTarget, + ErrorCategory, + recordLaunchInternal, + recordToolInvocation, + TOOL_NAMES, + ToolOutcome, +} from "./lmToolTelemetry"; // ============================================================================ // Constants @@ -63,14 +76,20 @@ export function registerLanguageModelTool(context: vscode.ExtensionContext): vsc const tool: LanguageModelTool = { async invoke(options: { input: DebugJavaApplicationInput }, token: vscode.CancellationToken): Promise { - sendInfo('', { - operationName: 'languageModelTool.debugJavaApplication.invoke', - target: options.input.target, - skipBuild: options.input.skipBuild?.toString() || 'false', - }); + const startedAt = Date.now(); + const targetType = classifyTarget(options.input.target); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; try { const result = await debugJavaApplication(options.input, token); + if (!result.success) { + outcome = result.status === 'timeout' ? 'timeout' : 'failure'; + errorCategory = result.success ? undefined : classifyError(result.message); + } else if (result.status === 'timeout') { + outcome = 'timeout'; + errorCategory = 'timeout'; + } // Format the message for AI - use simple text, not JSON const message = result.success @@ -82,13 +101,23 @@ export function registerLanguageModelTool(context: vscode.ExtensionContext): vsc new (vscode as any).LanguageModelTextPart(message) ]); } catch (error) { - sendError(error as Error); + outcome = token.isCancellationRequested ? 'cancelled' : 'failure'; + errorCategory = classifyError(error); const errorMessage = error instanceof Error ? error.message : String(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Debug failed: ${errorMessage}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.DEBUG_JAVA_APPLICATION, + outcome, + errorCategory, + targetType, + skipBuild: !!options.input.skipBuild, + durationMs: Date.now() - startedAt, + }); } } }; @@ -120,10 +149,9 @@ async function debugJavaApplication( // Step 0: Cleanup any existing Java debug session to avoid port conflicts const existingSession = vscode.debug.activeDebugSession; if (existingSession && existingSession.type === 'java') { - sendInfo('', { - operationName: 'languageModelTool.cleanupExistingSession', + recordLaunchInternal({ + name: 'cleanupExistingSession', sessionId: existingSession.id, - sessionName: existingSession.name }); try { await vscode.debug.stopDebugging(existingSession); @@ -131,9 +159,9 @@ async function debugJavaApplication( await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) { // Log but continue - the old session might already be dead - sendInfo('', { - operationName: 'languageModelTool.cleanupExistingSessionFailed', - error: String(error) + recordLaunchInternal({ + name: 'cleanupExistingSessionFailed', + errorCategory: classifyError(error), }); } } @@ -219,10 +247,9 @@ async function debugJavaApplication( clearTimeout(timeoutHandle); } - sendInfo('', { - operationName: 'languageModelTool.debugSessionStarted.eventBased', + recordLaunchInternal({ + name: 'debugSessionStarted.eventBased', sessionId: session.id, - sessionName: session.name }); resolve({ @@ -243,10 +270,7 @@ async function debugJavaApplication( if (!sessionStarted) { sessionDisposable.dispose(); - sendInfo('', { - operationName: 'languageModelTool.debugSessionTimeout.eventBased', - target: targetInfo - }); + recordLaunchInternal({ name: 'debugSessionTimeout.eventBased' }); resolve({ success: false, @@ -280,12 +304,13 @@ async function debugJavaApplication( // Check if debug session has started const session = vscode.debug.activeDebugSession; if (session && session.type === 'java') { - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + const elapsedMs = Date.now() - startTime; + const elapsedTime = (elapsedMs / 1000).toFixed(1); - sendInfo('', { - operationName: 'languageModelTool.debugSessionDetected', + recordLaunchInternal({ + name: 'debugSessionDetected', sessionId: session.id, - elapsedTime + elapsedMs, }); return { @@ -302,10 +327,9 @@ async function debugJavaApplication( } // Timeout: session not detected within 15 seconds - sendInfo('', { - operationName: 'languageModelTool.debugSessionTimeout.smartPolling', - target: targetInfo, - maxWaitTime + recordLaunchInternal({ + name: 'debugSessionTimeout.smartPolling', + maxWaitTime, }); return { @@ -584,19 +608,18 @@ function constructDebugCommand( if (!input.target.includes('.')) { const detectedClassName = findFullyQualifiedClassName(input.workspacePath, input.target, projectType); if (detectedClassName) { - sendInfo('', { - operationName: 'languageModelTool.classNameDetection', - simpleClassName: input.target, - detectedClassName, - projectType + recordLaunchInternal({ + name: 'classNameDetection', + projectType, + detected: true, }); className = detectedClassName; } else { // No package detected - class is in default package - sendInfo('', { - operationName: 'languageModelTool.classNameDetection.noPackage', - simpleClassName: input.target, - projectType + recordLaunchInternal({ + name: 'classNameDetection', + projectType, + detected: false, }); } } @@ -917,6 +940,11 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 1: Set Breakpoint const setBreakpointTool: LanguageModelTool = { async invoke(options: { input: SetBreakpointInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + const breakpointKind = classifyBreakpoint(options.input); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + try { const { filePath, lineNumber, condition, hitCondition, logMessage } = options.input; @@ -944,9 +972,19 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to set breakpoint: ${error}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.SET_JAVA_BREAKPOINT, + outcome, + errorCategory, + breakpointKind, + durationMs: Date.now() - startedAt, + }); } } }; @@ -955,9 +993,16 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 2: Step Operations const stepOperationTool: LanguageModelTool = { async invoke(options: { input: StepOperationInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + const stepKind = classifyStep(options.input.operation); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + try { const session = vscode.debug.activeDebugSession; if (!session || session.type !== 'java') { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No active Java debug session.') ]); @@ -975,6 +1020,14 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs }; const command = commandMap[operation]; + if (!command) { + outcome = 'failure'; + errorCategory = 'other'; + return new (vscode as any).LanguageModelToolResult([ + new (vscode as any).LanguageModelTextPart(`✗ Unknown step operation: ${operation}`) + ]); + } + if (threadId !== undefined) { // For thread-specific operations, use custom request await session.customRequest(operation, { threadId }); @@ -987,9 +1040,19 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs new (vscode as any).LanguageModelTextPart(`✓ Executed ${operation}`) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Step operation failed: ${error}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.DEBUG_STEP_OPERATION, + outcome, + errorCategory, + stepKind, + durationMs: Date.now() - startedAt, + }); } } }; @@ -998,9 +1061,17 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 3: Get Variables const getVariablesTool: LanguageModelTool = { async invoke(options: { input: GetVariablesInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + const scopeTypeEnum = classifyScopeType(options.input.scopeType); + const hasFilter = !!options.input.filter; + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + try { const session = vscode.debug.activeDebugSession; if (!session || session.type !== 'java') { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No active Java debug session.') ]); @@ -1018,6 +1089,8 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs } if (!targetThreadId) { + outcome = 'noSuspendedThread'; + errorCategory = 'noSuspendedThread'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No suspended thread found. Use get_debug_threads() to see thread states.') ]); @@ -1031,6 +1104,8 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs }); if (!stackResponse.stackFrames || stackResponse.stackFrames.length === 0) { + outcome = 'noStackFrame'; + errorCategory = 'noStackFrame'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No stack frame available.') ]); @@ -1075,9 +1150,20 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to get variables: ${error}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.GET_DEBUG_VARIABLES, + outcome, + errorCategory, + scopeType: scopeTypeEnum, + hasFilter, + durationMs: Date.now() - startedAt, + }); } } }; @@ -1086,9 +1172,16 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 4: Get Stack Trace const getStackTraceTool: LanguageModelTool = { async invoke(options: { input: GetStackTraceInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + let frameCount = 0; + try { const session = vscode.debug.activeDebugSession; if (!session || session.type !== 'java') { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No active Java debug session.') ]); @@ -1103,11 +1196,15 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs }); if (!stackResponse.stackFrames || stackResponse.stackFrames.length === 0) { + outcome = 'noStackFrame'; + errorCategory = 'noStackFrame'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('No stack frames available.') ]); } + frameCount = stackResponse.stackFrames.length; + const frames = stackResponse.stackFrames.map((frame: any, index: number) => { const location = frame.source ? `${frame.source.name}:${frame.line}` : @@ -1121,9 +1218,19 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to get stack trace: ${error}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.GET_DEBUG_STACK_TRACE, + outcome, + errorCategory, + frameCount, + durationMs: Date.now() - startedAt, + }); } } }; @@ -1132,9 +1239,16 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 5: Evaluate Expression const evaluateExpressionTool: LanguageModelTool = { async invoke(options: { input: EvaluateExpressionInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + const evalContext = classifyEvalContext(options.input.context); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + try { const session = vscode.debug.activeDebugSession; if (!session || session.type !== 'java') { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No active Java debug session.') ]); @@ -1168,6 +1282,8 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs targetFrameId = stackResponse.stackFrames[0].id; } } catch { + outcome = 'noSuspendedThread'; + errorCategory = 'noSuspendedThread'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Thread #${targetThreadId} is not suspended. Cannot evaluate expression.`) ]); @@ -1175,6 +1291,8 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs } if (!targetThreadId) { + outcome = 'noSuspendedThread'; + errorCategory = 'noSuspendedThread'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No suspended thread found. Use get_debug_threads() to see thread states.') ]); @@ -1194,9 +1312,20 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Evaluation failed: ${error}`) ]); + } finally { + // NEVER log expression text (may contain user code / secrets) + recordToolInvocation({ + tool: TOOL_NAMES.EVALUATE_DEBUG_EXPRESSION, + outcome, + errorCategory, + evalContext, + durationMs: Date.now() - startedAt, + }); } } }; @@ -1205,9 +1334,17 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 6: Get Threads const getThreadsTool: LanguageModelTool<{}> = { async invoke(_options: { input: {} }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + let threadCount = 0; + let suspendedCount = 0; + try { const session = vscode.debug.activeDebugSession; if (!session || session.type !== 'java') { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('✗ No active Java debug session.') ]); @@ -1221,6 +1358,8 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ]); } + threadCount = threadsResponse.threads.length; + // Check each thread's state by trying to get its stack trace const threadInfos: string[] = []; for (const thread of threadsResponse.threads) { @@ -1236,6 +1375,7 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs if (stackResponse?.stackFrames?.length > 0) { state = '🔴 SUSPENDED'; + suspendedCount++; const topFrame = stackResponse.stackFrames[0]; if (topFrame.source) { location = ` at ${topFrame.source.name}:${topFrame.line}`; @@ -1264,9 +1404,20 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to get threads: ${error}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.GET_DEBUG_THREADS, + outcome, + errorCategory, + threadCount, + suspendedCount, + durationMs: Date.now() - startedAt, + }); } } }; @@ -1275,6 +1426,12 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 7: Remove Breakpoints const removeBreakpointsTool: LanguageModelTool = { async invoke(options: { input: RemoveBreakpointsInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + const removeScope = classifyRemoveScope(options.input); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + let removedCount = 0; + try { const { filePath, lineNumber } = options.input; @@ -1283,6 +1440,7 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs if (!filePath) { // Remove all breakpoints (no active session required) const count = breakpoints.length; + removedCount = count; vscode.debug.removeBreakpoints(breakpoints); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✓ Removed all ${count} breakpoint(s).`) @@ -1304,6 +1462,7 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs if (toRemove.length > 0) { vscode.debug.removeBreakpoints(toRemove); } + removedCount = toRemove.length; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart( @@ -1313,9 +1472,20 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to remove breakpoints: ${error}`) ]); + } finally { + recordToolInvocation({ + tool: TOOL_NAMES.REMOVE_JAVA_BREAKPOINTS, + outcome, + errorCategory, + removeScope, + removedCount, + durationMs: Date.now() - startedAt, + }); } } }; @@ -1323,38 +1493,46 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 9: Stop Debug Session const stopDebugSessionTool: LanguageModelTool = { - async invoke(options: { input: StopDebugSessionInput }, _token: vscode.CancellationToken): Promise { + async invoke(_options: { input: StopDebugSessionInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + try { const session = vscode.debug.activeDebugSession; if (!session) { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart('No active debug session to stop.') ]); } - const sessionInfo = `${session.name} (${session.type})`; - const reason = options.input.reason || 'Investigation complete'; + const sessionType = session.type; // Stop the debug session await vscode.debug.stopDebugging(session); - sendInfo('', { - operationName: 'languageModelTool.stopDebugSession', - sessionId: session.id, - sessionName: session.name, - reason - }); - return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart( - `✓ Stopped debug session: ${sessionInfo}. Reason: ${reason}` + `✓ Stopped debug session (${sessionType}).` ) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to stop debug session: ${error}`) ]); + } finally { + // Do NOT log session.name (may include user file path) or input.reason (free text) + recordToolInvocation({ + tool: TOOL_NAMES.STOP_DEBUG_SESSION, + outcome, + errorCategory, + durationMs: Date.now() - startedAt, + }); } } }; @@ -1363,10 +1541,17 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // Tool 10: Get Debug Session Info const getDebugSessionInfoTool: LanguageModelTool = { async invoke(_options: { input: GetDebugSessionInfoInput }, _token: vscode.CancellationToken): Promise { + const startedAt = Date.now(); + let outcome: ToolOutcome = 'success'; + let errorCategory: ErrorCategory | undefined; + let isPausedFlag = false; + try { const session = vscode.debug.activeDebugSession; if (!session) { + outcome = 'noActiveSession'; + errorCategory = 'noActiveSession'; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart( '❌ No active debug session found.\n\n' + @@ -1457,9 +1642,9 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs // If we can't even get threads, something is wrong // But session exists, so mark as running isPaused = false; - sendInfo('', { - operationName: 'languageModelTool.getDebugSessionInfo.threadError', - error: String(error) + recordLaunchInternal({ + name: 'getDebugSessionInfo.threadError', + errorCategory: classifyError(error), }); } @@ -1539,23 +1724,26 @@ export function registerDebugSessionTools(_context: vscode.ExtensionContext): vs '═══════════════════════════════════════════' ].join('\n'); - sendInfo('', { - operationName: 'languageModelTool.getDebugSessionInfo', - sessionId: session.id, - sessionType: session.type, - isPaused: String(isPaused), - stoppedThreadId: String(stoppedThreadId || ''), - currentFile, - currentLine: String(currentLine) - }); + isPausedFlag = isPaused; return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(message) ]); } catch (error) { + outcome = 'failure'; + errorCategory = classifyError(error); return new (vscode as any).LanguageModelToolResult([ new (vscode as any).LanguageModelTextPart(`✗ Failed to get debug session info: ${error}`) ]); + } finally { + // Do NOT log currentFile, currentLine, sessionName, stoppedThreadName — those are user data + recordToolInvocation({ + tool: TOOL_NAMES.GET_DEBUG_SESSION_INFO, + outcome, + errorCategory, + isPaused: isPausedFlag, + durationMs: Date.now() - startedAt, + }); } } }; diff --git a/src/lmToolTelemetry.ts b/src/lmToolTelemetry.ts new file mode 100644 index 00000000..617e0b30 --- /dev/null +++ b/src/lmToolTelemetry.ts @@ -0,0 +1,450 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +/** + * Telemetry helpers for the language-model-tool surface. + * + * POLICY: this module is the ONLY place inside the LMT code path that is + * allowed to call `sendInfo` / `sendError`. Direct calls from individual + * tool implementations are forbidden so that PII risk can be audited in + * one file. + * + * Strict rules — every contributor MUST follow these: + * + * 1. Do NOT pass user-provided strings as telemetry properties. This + * includes (non-exhaustive): + * - `target` (main class / JAR path / raw -cp args) + * - `expression` (debug expression to evaluate) + * - `condition` / `hitCondition` / `logMessage` (breakpoint inputs) + * - `filePath` / `currentFile` / source file paths + * - `currentLine` / `lineNumber` + * - `sessionName` (`launch.json` `name` field; often contains class + * or project names) + * - `reason` (user-supplied stop reason) + * - `error.message` / `error.stack` (JVM stack traces leak user + * class and method names) + * - any class name, method name, package name, or source path + * + * 2. Only enums, booleans, durations, counts, opaque session IDs (GUIDs) + * and our own extension version are allowed. + * + * 3. When classifying free-form input (e.g. error text -> errorCategory) + * the classifier function inspects the input in-memory and emits ONLY + * the enum. Unmatched values map to `'other'` / `'unknown'`. The + * original text is NEVER attached to the event. + * + * 4. New telemetry events SHOULD go through `recordToolInvocation` / + * `recordChatActivation` or a new dedicated recorder added below. + * The raw `sendInfo` API is wrapped by `sanitizedSend` here. + */ + +import { sendInfo } from "vscode-extension-telemetry-wrapper"; + +// ============================================================================ +// Enum types (the only shape telemetry properties may take) +// ============================================================================ + +export type ToolOutcome = + | 'success' + | 'failure' + | 'timeout' + | 'cancelled' + | 'lsNotReady' + | 'noActiveSession' + | 'noSuspendedThread' + | 'noStackFrame'; + +export type ErrorCategory = + | 'mainClassMissing' + | 'classpathUnresolved' + | 'buildFailure' + | 'projectNotDetected' + | 'sessionAlreadyRunning' + | 'timeout' + | 'lsNotReady' + | 'noActiveSession' + | 'noSuspendedThread' + | 'noStackFrame' + | 'cancelled' + | 'other'; + +export type TargetType = 'mainClass' | 'jar' | 'rawArgs' | 'unknown'; + +export type BreakpointKind = + | 'line' + | 'conditional' + | 'hitCount' + | 'logpoint'; + +export type StepKind = 'in' | 'out' | 'over' | 'continue' | 'pause' | 'unknown'; + +export type EvalContext = 'watch' | 'repl' | 'hover' | 'unknown'; + +export type RemoveBreakpointScope = 'all' | 'file' | 'line'; + +export type ScopeType = 'local' | 'static' | 'all' | 'unknown'; + +export const TOOL_NAMES = { + DEBUG_JAVA_APPLICATION: 'debug_java_application', + SET_JAVA_BREAKPOINT: 'set_java_breakpoint', + DEBUG_STEP_OPERATION: 'debug_step_operation', + GET_DEBUG_VARIABLES: 'get_debug_variables', + GET_DEBUG_STACK_TRACE: 'get_debug_stack_trace', + EVALUATE_DEBUG_EXPRESSION: 'evaluate_debug_expression', + GET_DEBUG_THREADS: 'get_debug_threads', + REMOVE_JAVA_BREAKPOINTS: 'remove_java_breakpoints', + STOP_DEBUG_SESSION: 'stop_debug_session', + GET_DEBUG_SESSION_INFO: 'get_debug_session_info', +} as const; + +export type ToolName = typeof TOOL_NAMES[keyof typeof TOOL_NAMES]; + +// ============================================================================ +// Classifiers — pure functions; emit ONLY enums +// ============================================================================ + +/** + * Classify the `target` parameter of `debug_java_application` into a coarse + * shape category. The original string is consumed in-memory only; the + * returned enum is the only thing that may be logged. + */ +export function classifyTarget(target: string | undefined | null): TargetType { + if (!target) { + return 'unknown'; + } + const trimmed = target.trim(); + if (!trimmed) { + return 'unknown'; + } + if (trimmed.startsWith('-')) { + return 'rawArgs'; + } + if (/\.jar(\s|$)/i.test(trimmed) || trimmed.toLowerCase().endsWith('.jar')) { + return 'jar'; + } + if (/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/.test(trimmed)) { + return 'mainClass'; + } + return 'unknown'; +} + +/** + * Map an arbitrary error (Error, string, or unknown) to an ErrorCategory. + * The original message and stack trace are consumed in-memory and never + * returned. Unrecognised errors map to `'other'`. + */ +export function classifyError(err: unknown): ErrorCategory { + if (err === undefined || err === null) { + return 'other'; + } + const msg = (err instanceof Error ? err.message : String(err)).toLowerCase(); + if (!msg) { + return 'other'; + } + if (msg.includes('mainclass') && (msg.includes('not set') || msg.includes('missing') || msg.includes('not configured'))) { + return 'mainClassMissing'; + } + if (msg.includes('could not find or load main class') || msg.includes('classnotfound')) { + return 'mainClassMissing'; + } + if (msg.includes('classpath') && (msg.includes('not resolve') || msg.includes('unresolved') || msg.includes('cannot resolve'))) { + return 'classpathUnresolved'; + } + if (msg.includes('compilation') && msg.includes('fail')) { + return 'buildFailure'; + } + if (msg.includes('build failed') || msg.includes('build error')) { + return 'buildFailure'; + } + if (msg.includes('project not detected') || msg.includes('no project found')) { + return 'projectNotDetected'; + } + if (msg.includes('already running') || msg.includes('session is active')) { + return 'sessionAlreadyRunning'; + } + if (msg.includes('timeout') || msg.includes('timed out')) { + return 'timeout'; + } + if (msg.includes('language server not ready') || msg.includes('jdt.ls')) { + return 'lsNotReady'; + } + if (msg.includes('no active debug session') || msg.includes('no debug session')) { + return 'noActiveSession'; + } + if (msg.includes('not suspended') || msg.includes('thread is not paused')) { + return 'noSuspendedThread'; + } + if (msg.includes('cancel')) { + return 'cancelled'; + } + return 'other'; +} + +/** + * Classify a `set_java_breakpoint` invocation into a coarse breakpoint kind. + * The actual filePath / lineNumber / condition strings are NOT logged; this + * classifier only checks which optional inputs are present. + */ +export function classifyBreakpoint(input: { + condition?: string; + hitCondition?: string; + logMessage?: string; +}): BreakpointKind { + if (input.logMessage && input.logMessage.length > 0) { + return 'logpoint'; + } + if (input.hitCondition && input.hitCondition.length > 0) { + return 'hitCount'; + } + if (input.condition && input.condition.length > 0) { + return 'conditional'; + } + return 'line'; +} + +export function classifyStep(operation: string | undefined): StepKind { + switch (operation) { + case 'stepIn': + return 'in'; + case 'stepOut': + return 'out'; + case 'stepOver': + return 'over'; + case 'continue': + return 'continue'; + case 'pause': + return 'pause'; + default: + return 'unknown'; + } +} + +export function classifyEvalContext(context: string | undefined): EvalContext { + switch (context) { + case 'watch': + case 'repl': + case 'hover': + return context; + default: + return 'unknown'; + } +} + +export function classifyRemoveScope(input: { + filePath?: string; + lineNumber?: number; +}): RemoveBreakpointScope { + if (!input.filePath) { + return 'all'; + } + if (input.lineNumber !== undefined) { + return 'line'; + } + return 'file'; +} + +export function classifyScopeType(scopeType: string | undefined): ScopeType { + switch (scopeType) { + case 'local': + case 'static': + case 'all': + return scopeType; + default: + return 'unknown'; + } +} + +// ============================================================================ +// Recording helpers — the only entrypoints to `sendInfo` inside LMT code +// ============================================================================ + +/** Safe value types allowed as telemetry properties. */ +type SafeValue = string | number | boolean | undefined; + +/** + * Tighten what sendInfo accepts. All values must be primitive enums / + * booleans / numbers / well-known opaque IDs. Objects and arrays are + * rejected at the type level so we cannot accidentally serialise a payload + * containing user data. + */ +function sanitizedSend(properties: Record): void { + const clean: { [key: string]: string } = {}; + for (const [k, v] of Object.entries(properties)) { + if (v === undefined) { + continue; + } + clean[k] = typeof v === 'string' ? v : String(v); + } + sendInfo('', clean); +} + +export interface ToolInvocationRecord { + tool: ToolName; + outcome: ToolOutcome; + errorCategory?: ErrorCategory; + durationMs?: number; + /** + * Optional tool-specific enum fields. ONLY enums / booleans / numbers + * are accepted; the recorder itself is typed to forbid raw strings. + */ + targetType?: TargetType; + breakpointKind?: BreakpointKind; + stepKind?: StepKind; + evalContext?: EvalContext; + removeScope?: RemoveBreakpointScope; + scopeType?: ScopeType; + isPaused?: boolean; + skipBuild?: boolean; + hasFilter?: boolean; + frameCount?: number; + threadCount?: number; + suspendedCount?: number; + removedCount?: number; + /** Opaque GUID assigned by VS Code; safe to log. */ + sessionId?: string; + /** vscode-java-debug's own adapter type — value is constant `'java'`. */ + sessionType?: string; +} + +/** + * Record a single tool-invocation outcome. Replaces ad-hoc `sendInfo` + * calls inside individual tools. + * + * Before sending, the record is normalized so that `outcome` and + * `errorCategory` stay aligned for the six shared terminal values + * (cancelled / timeout / lsNotReady / noActiveSession / noSuspendedThread / + * noStackFrame). See {@link normalizeToolInvocationRecord}. + */ +export function recordToolInvocation(record: ToolInvocationRecord): void { + const normalized = normalizeToolInvocationRecord(record); + sanitizedSend({ + operationName: `languageModelTool.${normalized.tool}.invoke`, + outcome: normalized.outcome, + errorCategory: normalized.errorCategory, + durationMs: normalized.durationMs, + targetType: normalized.targetType, + breakpointKind: normalized.breakpointKind, + stepKind: normalized.stepKind, + evalContext: normalized.evalContext, + removeScope: normalized.removeScope, + scopeType: normalized.scopeType, + isPaused: normalized.isPaused, + skipBuild: normalized.skipBuild, + hasFilter: normalized.hasFilter, + frameCount: normalized.frameCount, + threadCount: normalized.threadCount, + suspendedCount: normalized.suspendedCount, + removedCount: normalized.removedCount, + sessionId: normalized.sessionId, + sessionType: normalized.sessionType, + }); +} + +/** + * Values that exist in both {@link ToolOutcome} and {@link ErrorCategory}. + * For these, the two fields must stay in lock-step so dashboard queries + * filtering on either one produce identical results. + */ +const SHARED_TERMINAL_VALUES = [ + 'cancelled', + 'timeout', + 'lsNotReady', + 'noActiveSession', + 'noSuspendedThread', + 'noStackFrame', +] as const; + +type SharedTerminal = typeof SHARED_TERMINAL_VALUES[number]; + +function isSharedTerminal(value: string | undefined): value is SharedTerminal { + return value !== undefined && (SHARED_TERMINAL_VALUES as readonly string[]).includes(value); +} + +/** + * Reconcile `outcome` and `errorCategory` for the six shared terminal + * values so downstream queries can rely on either field. Returns a NEW + * record; the input is not mutated. + * + * Rules: + * - If `errorCategory` is a shared terminal value, promote `outcome` to + * that value (callers that only set `errorCategory` get a consistent + * `outcome` for free). + * - If `outcome` is a shared terminal value and `errorCategory` is + * absent, fill it with the matching value (callers that only set + * `outcome` get a consistent `errorCategory`). + */ +function normalizeToolInvocationRecord(record: ToolInvocationRecord): ToolInvocationRecord { + let outcome: ToolOutcome = record.outcome; + let errorCategory: ErrorCategory | undefined = record.errorCategory; + + if (isSharedTerminal(errorCategory)) { + outcome = errorCategory; + } else if (isSharedTerminal(outcome) && errorCategory === undefined) { + errorCategory = outcome; + } + + return { ...record, outcome, errorCategory }; +} + +export interface ChatActivationRecord { + javaLSReadyAtActivation: boolean; + lmtCount: number; + chatSkillsCount: number; + chatInstructionsCount: number; + extensionVersion: string; +} + +/** + * Record a one-shot snapshot of the chat-activation surface at the moment + * Language Model Tools are registered. Lets us measure adoption coverage + * post-ship without per-turn cost. + */ +export function recordChatActivation(record: ChatActivationRecord): void { + sanitizedSend({ + operationName: 'languageModelTool.chatActivationSnapshot', + javaLSReadyAtActivation: record.javaLSReadyAtActivation, + lmtCount: record.lmtCount, + chatSkillsCount: record.chatSkillsCount, + chatInstructionsCount: record.chatInstructionsCount, + extensionVersion: record.extensionVersion, + }); +} + +/** + * Project type detected by the launch flow. Free-form values are + * forbidden so this stays a closed enum. + */ +export type LaunchProjectType = 'maven' | 'gradle' | 'vscode' | 'unknown'; + +/** + * Discriminated union of every launch-flow internal event the recorder + * is allowed to emit. Each variant lists its allowed properties so the + * type system rejects unknown event names and unknown property keys. + * + * Note: `sessionId` here is VS Code's opaque debug-session GUID, never + * the user-visible `launch.json` session name. + */ +export type LaunchInternalEvent = + | { name: 'cleanupExistingSession'; sessionId: string } + | { name: 'cleanupExistingSessionFailed'; errorCategory: ErrorCategory } + | { name: 'debugSessionStarted.eventBased'; sessionId: string } + | { name: 'debugSessionTimeout.eventBased' } + | { name: 'debugSessionDetected'; sessionId: string; elapsedMs: number } + | { name: 'debugSessionTimeout.smartPolling'; maxWaitTime: number } + | { name: 'classNameDetection'; projectType: LaunchProjectType; detected: boolean } + | { name: 'getDebugSessionInfo.threadError'; errorCategory: ErrorCategory }; + +/** + * Internal-debug event for the launch-flow nested instrumentation + * (session-detected / cleanup / timeout). Re-uses the sanitised sender so + * no PII can slip in. Accepts only the discriminated-union shapes defined + * in {@link LaunchInternalEvent} — unknown event names or unexpected + * property keys are rejected at compile time. + */ +export function recordLaunchInternal(event: LaunchInternalEvent): void { + const { name, ...properties } = event; + sanitizedSend({ + operationName: `languageModelTool.${name}`, + ...properties, + }); +} diff --git a/src/noConfigDebugInit.ts b/src/noConfigDebugInit.ts index b0da4f01..2f84ee6b 100644 --- a/src/noConfigDebugInit.ts +++ b/src/noConfigDebugInit.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { sendInfo, sendError } from "vscode-extension-telemetry-wrapper"; import { getJavaHome } from "./utility"; +import { buildNoConfigPathAppendValue } from "./pathUtil"; /** * Registers the configuration-less debugging setup for the extension. @@ -91,14 +92,7 @@ export async function registerNoConfigDebug( } const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); - const pathSeparator = process.platform === 'win32' ? ';' : ':'; - - // Check if the current PATH already ends with a path separator to avoid double separators - const currentPath = process.env.PATH || ''; - const needsSeparator = currentPath.length > 0 && !currentPath.endsWith(pathSeparator); - const pathValueToAppend = needsSeparator ? `${pathSeparator}${noConfigScriptsDir}` : noConfigScriptsDir; - - collection.append('PATH', pathValueToAppend); + collection.append('PATH', buildNoConfigPathAppendValue(noConfigScriptsDir)); // create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written const fileSystemWatcher = vscode.workspace.createFileSystemWatcher( diff --git a/src/pathUtil.ts b/src/pathUtil.ts new file mode 100644 index 00000000..a9f680b3 --- /dev/null +++ b/src/pathUtil.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +/** + * Builds the value to append to PATH for the noConfigScripts directory. + * + * `vscode.EnvironmentVariableCollection.append()` does literal string + * concatenation, so we always prepend a separator to avoid gluing our + * directory onto the last entry of the user's PATH (see issue #1637). + * + * @param platform defaults to `process.platform`; injectable for tests. + */ +export function buildNoConfigPathAppendValue( + scriptsDir: string, + platform: NodeJS.Platform = process.platform, +): string { + const pathSeparator = platform === 'win32' ? ';' : ':'; + return `${pathSeparator}${scriptsDir}`; +} diff --git a/src/progressImpl.ts b/src/progressImpl.ts index 6ee2a98c..48626936 100644 --- a/src/progressImpl.ts +++ b/src/progressImpl.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { v4 } from "uuid"; +import * as crypto from "crypto"; import { CancellationToken, CancellationTokenSource, Disposable, EventEmitter, ProgressLocation, StatusBarAlignment, StatusBarItem, window, workspace } from "vscode"; import { IProgressProvider, IProgressReporter } from "./progressAPI"; const STATUS_COMMAND: string = "java.show.server.task.status"; class ProgressReporter implements IProgressReporter { - private _id: string = v4(); + private _id: string = crypto.randomUUID(); private _jobName: string; private _progressLocation: ProgressLocation | { viewId: string }; private _cancellable: boolean = false; diff --git a/test/pathUtil.test.ts b/test/pathUtil.test.ts new file mode 100644 index 00000000..9f76c8f2 --- /dev/null +++ b/test/pathUtil.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as assert from "assert"; + +import { buildNoConfigPathAppendValue } from "../src/pathUtil"; + +// Regression tests for issue #1637. +suite("buildNoConfigPathAppendValue", () => { + + const winDir = "C:\\Users\\me\\.vscode\\extensions\\vscjava.vscode-java-debug-0.59.0\\bundled\\scripts\\noConfigScripts"; + const posixDir = "/home/me/.vscode/extensions/vscjava.vscode-java-debug-0.59.0/bundled/scripts/noConfigScripts"; + + test("uses ';' as separator on Windows", () => { + const result = buildNoConfigPathAppendValue(winDir, "win32"); + assert.strictEqual(result, `;${winDir}`); + }); + + test("uses ':' as separator on Linux", () => { + const result = buildNoConfigPathAppendValue(posixDir, "linux"); + assert.strictEqual(result, `:${posixDir}`); + }); + + test("uses ':' as separator on macOS", () => { + const result = buildNoConfigPathAppendValue(posixDir, "darwin"); + assert.strictEqual(result, `:${posixDir}`); + }); + + test("always starts with a path separator (Windows)", () => { + const result = buildNoConfigPathAppendValue(winDir, "win32"); + assert.ok(result.startsWith(";")); + }); + + test("always starts with a path separator (POSIX)", () => { + const result = buildNoConfigPathAppendValue(posixDir, "linux"); + assert.ok(result.startsWith(":")); + }); + + test("never collapses scriptsDir into the previous PATH entry on Windows", () => { + // #1637 scenario: last user PATH entry has no trailing separator. + const userPath = "C:\\foo;C:\\Program Files\\jreleaser\\"; + const entries = (userPath + buildNoConfigPathAppendValue(winDir, "win32")).split(";"); + assert.ok(entries.includes("C:\\Program Files\\jreleaser\\")); + assert.ok(entries.includes(winDir)); + }); + + test("never collapses scriptsDir into the previous PATH entry on POSIX", () => { + const userPath = "/usr/bin:/opt/jreleaser/bin"; + const entries = (userPath + buildNoConfigPathAppendValue(posixDir, "linux")).split(":"); + assert.ok(entries.includes("/opt/jreleaser/bin")); + assert.ok(entries.includes(posixDir)); + }); + + test("yields only an empty (harmless) entry when the user's PATH already ends with a separator", () => { + const userPath = "C:\\foo;C:\\bar;"; + const entries = (userPath + buildNoConfigPathAppendValue(winDir, "win32")).split(";"); + assert.ok(entries.includes(winDir)); + assert.ok(!entries.some((e) => e !== winDir && e.endsWith(winDir))); + }); + + test("scriptsDir appears unchanged at the end of the appended value", () => { + const result = buildNoConfigPathAppendValue(winDir, "win32"); + assert.ok(result.endsWith(winDir)); + }); +});