From cdd5636b180f6338602209ffe15dec784039af69 Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 26 Apr 2026 15:26:36 +0200 Subject: [PATCH] feat: use vsts-task-installer User-Agent for NuGet download statistics Change the NuGet download User-Agent from custom 'ALCops-AzureDevOps' to 'vsts-task-installer/{version} (Node.js {v}; {os} {release})' which matches a recognized known client pattern in NuGet.org's CDN log parser (knownclients.yaml), making ALCops.Analyzers downloads visible in per-package statistics and distinguishable from the VS Code extension. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 +- shared/user-agent.ts | 10 ++++++++++ tasks/install-analyzers/src/nuget-api.ts | 5 ++++- tests/install-analyzers/nuget-api.test.ts | 2 +- tests/shared/user-agent.test.ts | 20 ++++++++++++++++++++ 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 shared/user-agent.ts create mode 100644 tests/shared/user-agent.test.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1a23d1b..ff36014 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -63,7 +63,7 @@ The install-analyzers task interacts with NuGet via two APIs: Key design decisions: - `parseRegistrationIndex()` is a pure function (no I/O) for easy testing - `queryNuGetRegistration()` is a shared module usable by any task needing NuGet version info -- `User-Agent: ALCops-AzureDevOps` is set on all HTTP requests for NuGet.org statistics tracking +- `User-Agent: vsts-task-installer/{version}` is set on NuGet HTTP requests, matching a known client pattern in NuGet.org's CDN log parser for download statistics visibility - Unlisted versions are filtered out during version resolution - `resolveVersion()` returns a `ResolvedVersion` with both the version string and the `packageContentUrl` from the Registration API (avoids redundant URL construction) diff --git a/shared/user-agent.ts b/shared/user-agent.ts new file mode 100644 index 0000000..82c9b40 --- /dev/null +++ b/shared/user-agent.ts @@ -0,0 +1,10 @@ +import * as os from 'os'; + +/** + * Build the User-Agent string sent on NuGet HTTP requests. + * Uses the `vsts-task-installer` known-client pattern recognised by NuGet.org's + * CDN log parser so downloads appear in per-package statistics. + */ +export function getUserAgent(version: string): string { + return `vsts-task-installer/${version} (Node.js ${process.version}; ${os.type()} ${os.release()})`; +} diff --git a/tasks/install-analyzers/src/nuget-api.ts b/tasks/install-analyzers/src/nuget-api.ts index cd41c3a..b4d603e 100644 --- a/tasks/install-analyzers/src/nuget-api.ts +++ b/tasks/install-analyzers/src/nuget-api.ts @@ -5,9 +5,12 @@ import { NUGET_PACKAGE_NAME, NUGET_FLAT_CONTAINER, RegistrationVersion } from '. import { Logger, nullLogger } from '../../../shared/logger'; import { queryNuGetRegistration } from '../../../shared/nuget-registration'; import { httpsGetBuffer } from '../../../shared/http-client'; +import { getUserAgent } from '../../../shared/user-agent'; +import taskJson from '../task.json'; const packageId = NUGET_PACKAGE_NAME.toLowerCase(); -const USER_AGENT = 'ALCops-AzureDevOps'; +const { Major, Minor, Patch } = taskJson.version; +const USER_AGENT = getUserAgent(`${Major}.${Minor}.${Patch}`); export interface ResolvedVersion { version: string; diff --git a/tests/install-analyzers/nuget-api.test.ts b/tests/install-analyzers/nuget-api.test.ts index 91e70bc..85bb106 100644 --- a/tests/install-analyzers/nuget-api.test.ts +++ b/tests/install-analyzers/nuget-api.test.ts @@ -222,7 +222,7 @@ describe('downloadPackage', () => { try { await downloadPackage('1.0.0', tmpDir); const calledOpts = mockRequest.mock.calls[0][1] as { headers?: Record }; - expect(calledOpts.headers?.['User-Agent']).toBe('ALCops-AzureDevOps'); + expect(calledOpts.headers?.['User-Agent']).toMatch(/^vsts-task-installer\/\d+\.\d+\.\d+ \(Node\.js v/); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } diff --git a/tests/shared/user-agent.test.ts b/tests/shared/user-agent.test.ts new file mode 100644 index 0000000..2ecfbed --- /dev/null +++ b/tests/shared/user-agent.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { getUserAgent } from '../../shared/user-agent'; + +describe('getUserAgent', () => { + it('returns vsts-task-installer format with version', () => { + const ua = getUserAgent('1.2.3'); + expect(ua).toMatch(/^vsts-task-installer\/1\.2\.3 \(Node\.js v\d+\.\d+\.\d+; \w+ .+\)$/); + }); + + it('includes process.version and os info', () => { + const ua = getUserAgent('0.0.1'); + expect(ua).toContain(`Node.js ${process.version}`); + expect(ua).toContain('vsts-task-installer/0.0.1'); + }); + + it('works with pre-release style versions', () => { + const ua = getUserAgent('2.0.0-beta.1'); + expect(ua).toMatch(/^vsts-task-installer\/2\.0\.0-beta\.1 \(/); + }); +});