From eb280b556f1ad1fc0d6c3badcf46a10aad361134 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 26 Apr 2026 14:10:22 +0200 Subject: [PATCH 1/3] feat: use NuGet VS VSIX User-Agent for download statistics visibility Change the NuGet download User-Agent from custom 'ALCops-VSCode/{version}' to 'NuGet VS VSIX/{version} (Node.js {v}; {os} {release})' which matches a recognized known client pattern in NuGet.org's CDN log parser (knownclients.yaml), making downloads visible in per-package statistics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ src/downloader.ts | 2 +- tests/downloader.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e0658..f859195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to the ALCops extension will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.3] - 2026-04-26 + +### Changed +- Change NuGet download User-Agent from `ALCops-VSCode/{version}` to `NuGet VS VSIX/{version}` format with OS info, matching a recognized NuGet.org known client pattern for download statistics visibility + ## [1.3.2] - 2026-04-23 ### Changed diff --git a/src/downloader.ts b/src/downloader.ts index eef1775..2c9db31 100644 --- a/src/downloader.ts +++ b/src/downloader.ts @@ -20,7 +20,7 @@ const PACKAGE_NAME = 'ALCops.Analyzers'; function getUserAgent(): string { const extension = vscode.extensions.getExtension('arthurvdv.alcops'); const version = extension?.packageJSON?.version ?? '0.0.0'; - return `ALCops-VSCode/${version}`; + return `NuGet VS VSIX/${version} (Node.js ${process.version}; ${os.type()} ${os.release()})`; } class InstallationMutex { diff --git a/tests/downloader.test.ts b/tests/downloader.test.ts index cd034d7..c2f6fed 100644 --- a/tests/downloader.test.ts +++ b/tests/downloader.test.ts @@ -401,4 +401,28 @@ describe('queryNuGetRegistration', () => { await expect(queryNuGetRegistration('nonexistent')).rejects.toThrow('HTTP 404'); }); + + it('sends NuGet VS VSIX User-Agent header with version and OS info', async () => { + const indexBody: RegistrationIndex = { + items: [{ + '@id': 'https://api.nuget.org/v3/registration5-gz-semver2/test/index.json#page/0', + items: [{ + catalogEntry: { version: '1.0.0', listed: true }, + packageContent: 'https://api.nuget.org/v3-flatcontainer/test/1.0.0/test.1.0.0.nupkg', + }], + }], + }; + + let capturedOpts: Record = {}; + mockHttpsGet.mockImplementation((_url: unknown, opts: unknown, cb: unknown) => { + capturedOpts = opts as Record; + (cb as (r: unknown) => void)(createMockResponse(indexBody)); + const req = new EventEmitter(); + return Object.assign(req, { on: vi.fn().mockReturnThis() }); + }); + + await queryNuGetRegistration('Test'); + const headers = capturedOpts.headers as Record; + expect(headers['User-Agent']).toMatch(/^NuGet VS VSIX\/\d+\.\d+\.\d+\S* \(Node\.js v\d+\.\d+\.\d+; /); + }); }); From 77854fe9ebd8ebf73292917afc15469607d1673c Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 26 Apr 2026 14:14:12 +0200 Subject: [PATCH 2/3] docs: add NuGet User-Agent instructions for Azure DevOps extension Documents how NuGet.org download statistics parsing works, which known client patterns are available, and implementation checklist for the Azure DevOps extension. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../nuget-useragent.instructions.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .github/instructions/nuget-useragent.instructions.md diff --git a/.github/instructions/nuget-useragent.instructions.md b/.github/instructions/nuget-useragent.instructions.md new file mode 100644 index 0000000..135665c --- /dev/null +++ b/.github/instructions/nuget-useragent.instructions.md @@ -0,0 +1,128 @@ +--- +applyTo: '**' +--- + +# TODO: NuGet User-Agent for Azure DevOps Extension + +## Context + +The ALCops project has two extensions that download NuGet packages from NuGet.org: + +1. **VS Code Extension** (this repo) — already sends `NuGet VS VSIX/{version} (Node.js {v}; {os} {release})` as User-Agent ([PR #34](https://github.com/ALCops/vscode-extension/pull/34)) +2. **Azure DevOps Extension** — needs the same treatment with a different client name + +The goal is to make downloads from each extension visible and distinguishable in [NuGet.org per-package statistics](https://www.nuget.org/stats/packages/ALCops.Analyzers?groupby=ClientName&groupby=ClientVersion). + +## How NuGet.org Download Statistics Work + +NuGet.org processes download statistics by parsing **Azure CDN logs** using a Python-based User-Agent parser. The parser has three stages: + +1. **Known clients parser** — regex patterns defined in [`knownclients.yaml`](https://github.com/NuGet/NuGetGallery/blob/main/python/StatsLogParser/loginterpretation/knownclients.yaml) +2. **China CDN parser** — same patterns but with `+` replacing spaces (CDN URL-encodes spaces as `+`) +3. **Default `ua-parser` library** — designed for web browsers, does NOT recognize custom `Product/Version` strings + +If a User-Agent does not match any known client pattern or browser pattern, it is classified as **"Other"** and hidden from the stats page. + +### Key files in [NuGet/NuGetGallery](https://github.com/NuGet/NuGetGallery): + +| File | Purpose | +|---|---| +| `python/StatsLogParser/loginterpretation/knownclients.yaml` | Regex patterns for recognized clients | +| `python/StatsLogParser/loginterpretation/useragentparser.py` | Parser logic (known clients → China CDN → default ua-parser) | +| `python/StatsLogParser/tests/test_useragentparser.py` | Test cases for each known client | +| `src/Stats.AzureCdnLogs.Common/CdnLogEntryParser.cs` | CDN log line parser (User-Agent is column 14) | + +### Stats page dimensions + +The stats page groups by **ClientName** and **ClientVersion** only. OS info in parentheses is captured in CDN logs but NOT exposed as a separate dimension. + +## Known Client Patterns Available + +From `knownclients.yaml`, these are patterns that could be reused for Azure DevOps: + +```yaml +# Already used by VS Code extension: +- regex: '(NuGet VS VSIX)/(\d+)\.(\d+)\.?(\d+)?' + family_replacement: 'NuGet VS VSIX' + +# Potentially suitable for Azure DevOps extension: +- regex: '(vsts-task-installer)/(\d+)\.(\d+)\.?(\d+)?' + family_replacement: 'vsts-task-installer' + +# Other options (less fitting): +- regex: '(NuGet MSBuild Task)/(\d+)\.(\d+)\.?(\d+)?' + family_replacement: 'NuGet MSBuild Task' +- regex: '(NuGet .NET Core MSBuild Task)/(\d+)\.(\d+)\.?(\d+)?' + family_replacement: 'NuGet .NET Core MSBuild Task' +``` + +### Recommended: `vsts-task-installer` + +For the Azure DevOps extension, `vsts-task-installer` is the best fit because: +- It literally means "VSTS (Azure DevOps) task installer" +- The Azure DevOps extension IS a task that installs NuGet packages +- ALCops.Analyzers is niche, so actual `vsts-task-installer` downloads for this package should be zero +- The ALCops version numbers distinguish it from the real client + +**User-Agent format to use:** +``` +vsts-task-installer/{alcops_version} (Node.js {nodeVersion}; {osType} {osRelease}) +``` + +Example: `vsts-task-installer/1.3.3 (Node.js v22.0.0; Linux 5.15.0-1064-azure)` + +### Verification + +The regex `(vsts-task-installer)/(\d+)\.(\d+)\.?(\d+)?` matches via `re.search()`, so anything after the version (like the OS info in parens) is ignored by the parser. The family will be `vsts-task-installer` and the version groups capture the semver. + +## What Was Tested + +We installed and tested the actual `ua-parser` Python library used by NuGet.org. Results: + +- `ALCops-VSCode/1.3.2` → **Other** (not recognized) +- `ALCops/1.3.2` → **Other** (not recognized) +- `ALCops VSCode Extension/1.3.2 (...)` → **Other** (not recognized) +- Any custom `Product/Version` format → **Other** + +**Conclusion: there is no client-side-only User-Agent format that makes a custom client visible in NuGet.org stats. You must either mimic a known client or submit a PR to `knownclients.yaml`.** + +## Alternative: Submit a PR to NuGetGallery + +If you prefer a proper client name (e.g., "ALCops Azure DevOps Extension") instead of mimicking an existing one, submit a PR to [NuGet/NuGetGallery](https://github.com/NuGet/NuGetGallery). External PRs are regularly accepted: + +- [Bonsai PR #10447](https://github.com/NuGet/NuGetGallery/pull/10447) — merged in 5 days (May 2025) +- GetNuTool — added March 2026 + +The PR is a 2-file change: +1. Add regex to `python/StatsLogParser/loginterpretation/knownclients.yaml` +2. Add test case to `python/StatsLogParser/tests/test_useragentparser.py` + +## Implementation Checklist for Azure DevOps Extension + +- [ ] Find the HTTP download function (equivalent to `getUserAgent()` + `httpsGetWithRedirects()` in the VS Code extension's `src/downloader.ts`) +- [ ] Change the User-Agent header to: `vsts-task-installer/{version} (Node.js {process.version}; {os.type()} {os.release()})` +- [ ] Add/update unit tests to verify the User-Agent header format +- [ ] Update CHANGELOG +- [ ] Verify the User-Agent is sent on both NuGet API queries AND package downloads (both endpoints go through Azure CDN) + +## Current VS Code Extension Implementation (reference) + +In `src/downloader.ts`: + +```typescript +function getUserAgent(): string { + const extension = vscode.extensions.getExtension('arthurvdv.alcops'); + const version = extension?.packageJSON?.version ?? '0.0.0'; + return `NuGet VS VSIX/${version} (Node.js ${process.version}; ${os.type()} ${os.release()})`; +} +``` + +Used in `httpsGetWithRedirects()`: + +```typescript +https.get(url, { headers: { 'User-Agent': getUserAgent() } }, (response) => { ... }); +``` + +Applied to both: +- NuGet Registration API queries (`api.nuget.org/v3/registration5-gz-semver2/...`) +- Package downloads (`api.nuget.org/v3-flatcontainer/...`) From c9654dc3dcc9a401a68fd8009d111f1af3234dc7 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 26 Apr 2026 14:19:00 +0200 Subject: [PATCH 3/3] docs: remove TODO prefix from instructions heading Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/instructions/nuget-useragent.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/instructions/nuget-useragent.instructions.md b/.github/instructions/nuget-useragent.instructions.md index 135665c..72a8389 100644 --- a/.github/instructions/nuget-useragent.instructions.md +++ b/.github/instructions/nuget-useragent.instructions.md @@ -2,7 +2,7 @@ applyTo: '**' --- -# TODO: NuGet User-Agent for Azure DevOps Extension +# NuGet User-Agent for Azure DevOps Extension ## Context