From 3cecb298030c459a850326d04cca119ec6dc0e27 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 28 Aug 2025 18:26:13 +0200 Subject: [PATCH] Add REST rate limit logging (#814) --- .../node/copilotTokenManager.ts | 4 +- .../test/node/copilotToken.spec.ts | 2 +- .../vscode-node/copilotTokenManager.ts | 2 +- .../src/platform/github/common/githubAPI.ts | 44 +++++++++++++++++++ .../platform/github/common/githubService.ts | 22 +++------- .../github/common/octoKitServiceImpl.ts | 8 +++- .../github/node/githubRepositoryService.ts | 28 +++++------- 7 files changed, 71 insertions(+), 39 deletions(-) create mode 100644 extensions/copilot/src/platform/github/common/githubAPI.ts diff --git a/extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts b/extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts index 8e2a31678b7..7f1996f2c72 100644 --- a/extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts +++ b/extensions/copilot/src/platform/authentication/node/copilotTokenManager.ts @@ -227,7 +227,7 @@ export class FixedCopilotTokenManager extends BaseCopilotTokenManager implements @IFetcherService fetcherService: IFetcherService, @IEnvService envService: IEnvService ) { - super(new NullBaseOctoKitService(capiClientService, fetcherService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); + super(new NullBaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); this.copilotToken = { token: _completionsToken, expires_at: 0, refresh_in: 0, username: 'fixedTokenManager', isVscodeTeamMember: false, copilot_plan: 'unknown' }; } @@ -270,7 +270,7 @@ export class CopilotTokenManagerFromGitHubToken extends BaseCopilotTokenManager @IEnvService envService: IEnvService, @IConfigurationService protected readonly configurationService: IConfigurationService ) { - super(new NullBaseOctoKitService(capiClientService, fetcherService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); + super(new NullBaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); } async getCopilotToken(force?: boolean): Promise { diff --git a/extensions/copilot/src/platform/authentication/test/node/copilotToken.spec.ts b/extensions/copilot/src/platform/authentication/test/node/copilotToken.spec.ts index 1235d5da047..c7cd8cd9ced 100644 --- a/extensions/copilot/src/platform/authentication/test/node/copilotToken.spec.ts +++ b/extensions/copilot/src/platform/authentication/test/node/copilotToken.spec.ts @@ -32,7 +32,7 @@ class RefreshFakeCopilotTokenManager extends BaseCopilotTokenManager { @IFetcherService fetcherService: IFetcherService, @IEnvService envService: IEnvService, ) { - super(new NullBaseOctoKitService(capiClientService, fetcherService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); + super(new NullBaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); } async getCopilotToken(force?: boolean): Promise { diff --git a/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts b/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts index 1981eb20153..3b74afde8ed 100644 --- a/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts +++ b/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts @@ -39,7 +39,7 @@ export class VSCodeCopilotTokenManager extends BaseCopilotTokenManager { @IEnvService envService: IEnvService, @IConfigurationService protected readonly configurationService: IConfigurationService ) { - super(new BaseOctoKitService(capiClientService, fetcherService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); + super(new BaseOctoKitService(capiClientService, fetcherService, logService, telemetryService), logService, telemetryService, domainService, capiClientService, fetcherService, envService); } async getCopilotToken(force?: boolean): Promise { diff --git a/extensions/copilot/src/platform/github/common/githubAPI.ts b/extensions/copilot/src/platform/github/common/githubAPI.ts new file mode 100644 index 00000000000..32fd5d6d1f0 --- /dev/null +++ b/extensions/copilot/src/platform/github/common/githubAPI.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService } from '../../log/common/logService'; +import { IFetcherService } from '../../networking/common/fetcherService'; +import { ITelemetryService } from '../../telemetry/common/telemetry'; + + +export async function makeGitHubAPIRequest(fetcherService: IFetcherService, logService: ILogService, telemetry: ITelemetryService, host: string, routeSlug: string, method: 'GET' | 'POST', token: string | undefined, body?: { [key: string]: any }) { + const headers: any = { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetcherService.fetch(`${host}/${routeSlug}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined + }); + if (!response.ok) { + return undefined; + } + + try { + const result = await response.json(); + const rateLimit = Number(response.headers.get('x-ratelimit-remaining')); + const logMessage = `[RateLimit] REST rate limit remaining: ${rateLimit}, ${routeSlug}`; + if (rateLimit < 1000) { + // Danger zone + logService.warn(logMessage); + telemetry.sendMSFTTelemetryEvent('githubAPI.approachingRateLimit', { rateLimit: rateLimit.toString() }); + } else { + logService.debug(logMessage); + } + return result; + } catch { + return undefined; + } +} \ No newline at end of file diff --git a/extensions/copilot/src/platform/github/common/githubService.ts b/extensions/copilot/src/platform/github/common/githubService.ts index 50834bd8434..e9b84509598 100644 --- a/extensions/copilot/src/platform/github/common/githubService.ts +++ b/extensions/copilot/src/platform/github/common/githubService.ts @@ -5,7 +5,10 @@ import { createServiceIdentifier } from '../../../util/common/services'; import { ICAPIClientService } from '../../endpoint/common/capiClient'; +import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; +import { ITelemetryService } from '../../telemetry/common/telemetry'; +import { makeGitHubAPIRequest } from './githubAPI'; export const IGithubRepositoryService = createServiceIdentifier('IGithubRepositoryService'); export const IOctoKitService = createServiceIdentifier('IOctoKitService'); @@ -65,7 +68,9 @@ export interface IOctoKitService { export class BaseOctoKitService { constructor( private readonly _capiClientService: ICAPIClientService, - private readonly _fetcherService: IFetcherService + private readonly _fetcherService: IFetcherService, + private readonly _logService: ILogService, + private readonly _telemetryService: ITelemetryService ) { } async getCurrentAuthedUserWithToken(token: string): Promise { @@ -77,19 +82,6 @@ export class BaseOctoKitService { } protected async _makeGHAPIRequest(routeSlug: string, method: 'GET' | 'POST', token: string, body?: { [key: string]: any }) { - const response = await this._fetcherService.fetch(`${this._capiClientService.dotcomAPIURL}/${routeSlug}`, { - method, - headers: { 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'Authorization': `Bearer ${token}` }, - body: body ? JSON.stringify(body) : undefined - }); - if (!response.ok) { - return undefined; - } - - try { - return await response.json(); - } catch { - return undefined; - } + return makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, this._capiClientService.dotcomAPIURL, routeSlug, method, token, body); } } diff --git a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts index eac8fa6cac1..3ba5ab109f5 100644 --- a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts +++ b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IAuthenticationService } from '../../authentication/common/authentication'; import { ICAPIClientService } from '../../endpoint/common/capiClient'; +import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; +import { ITelemetryService } from '../../telemetry/common/telemetry'; import { BaseOctoKitService, IOctoKitService, IOctoKitUser } from './githubService'; export class OctoKitService extends BaseOctoKitService implements IOctoKitService { @@ -13,9 +15,11 @@ export class OctoKitService extends BaseOctoKitService implements IOctoKitServic constructor( @IAuthenticationService private readonly _authService: IAuthenticationService, @ICAPIClientService capiClientService: ICAPIClientService, - @IFetcherService fetcherService: IFetcherService + @IFetcherService fetcherService: IFetcherService, + @ILogService logService: ILogService, + @ITelemetryService telemetryService: ITelemetryService ) { - super(capiClientService, fetcherService); + super(capiClientService, fetcherService, logService, telemetryService); } async getCurrentAuthedUser(): Promise { diff --git a/extensions/copilot/src/platform/github/node/githubRepositoryService.ts b/extensions/copilot/src/platform/github/node/githubRepositoryService.ts index 97d907736da..0121c9d6632 100644 --- a/extensions/copilot/src/platform/github/node/githubRepositoryService.ts +++ b/extensions/copilot/src/platform/github/node/githubRepositoryService.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IAuthenticationService } from '../../authentication/common/authentication'; +import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; +import { ITelemetryService } from '../../telemetry/common/telemetry'; +import { makeGitHubAPIRequest } from '../common/githubAPI'; import { GithubRepositoryItem, IGithubRepositoryService } from '../common/githubService'; export class GithubRepositoryService implements IGithubRepositoryService { @@ -15,20 +18,15 @@ export class GithubRepositoryService implements IGithubRepositoryService { constructor( @IFetcherService private readonly _fetcherService: IFetcherService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @ILogService private readonly _logService: ILogService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { } private async _doGetRepositoryInfo(owner: string, repo: string) { const authToken: string | undefined = this._authenticationService.permissiveGitHubSession?.accessToken ?? this._authenticationService.anyGitHubSession?.accessToken; - const headers: Record = { - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28' - }; - if (authToken) { - headers['Authorization'] = `Bearer ${authToken}`; - } - // cache this based on creation info - return this._fetcherService.fetch(`https://api.github.com/repos/${owner}/${repo}`, { method: 'GET', headers }); + + return makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, 'https://api.github.com', `repos/${owner}/${repo}`, 'GET', authToken); } async getRepositoryInfo(owner: string, repo: string) { @@ -60,11 +58,7 @@ export class GithubRepositoryService implements IGithubRepositoryService { try { const authToken = this._authenticationService.permissiveGitHubSession?.accessToken; const encodedPath = path.split('/').map((segment) => encodeURIComponent(segment)).join('/'); - - const response = await this._fetcherService.fetch(`https://api.github.com/repos/${org}/${repo}/contents/${encodedPath}`, { - method: 'GET', - headers: { 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'Authorization': `Bearer ${authToken}` } - }); + const response = await makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, 'https://api.github.com', `repos/${org}/${repo}/contents/${encodedPath}`, 'GET', authToken); if (response.ok) { const data = (await response.json()); @@ -93,10 +87,8 @@ export class GithubRepositoryService implements IGithubRepositoryService { try { const authToken = this._authenticationService.permissiveGitHubSession?.accessToken; const encodedPath = path.split('/').map((segment) => encodeURIComponent(segment)).join('/'); - const response = await this._fetcherService.fetch(`https://api.github.com/repos/${org}/${repo}/contents/${encodedPath}`, { - method: 'GET', - headers: { 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'Authorization': `Bearer ${authToken}` } - }); + const response = await makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, 'https://api.github.com', `repos/${org}/${repo}/contents/${encodedPath}`, 'GET', authToken); + if (response.ok) { const data = (await response.json());