Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 1 addition & 47 deletions src/github/githubRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
User,
} from './interface';
import { IssueChangeEvent, IssueModel } from './issueModel';
import { LoggingOctokit } from './loggingOctokit';
import { getErrorCode, LoggingOctokit } from './loggingOctokit';
import { PullRequestModel } from './pullRequestModel';
import defaultSchema from './queries.gql';
import * as extraSchema from './queriesExtra.gql';
Expand Down Expand Up @@ -144,52 +144,6 @@ export function isRateLimitError(e: unknown): boolean {
return false;
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

export function getErrorCode(e: unknown): string | undefined {
if (!isObject(e)) {
return undefined;
}

if (e.status !== undefined) {
return String(e.status);
}

const networkError = e.networkError;
if (isObject(networkError) && networkError.statusCode !== undefined) {
return String(networkError.statusCode);
}

const graphQLErrors = e.graphQLErrors;
if (Array.isArray(graphQLErrors)) {
const firstGraphQLError = graphQLErrors[0];
if (isObject(firstGraphQLError)) {
const extensions = firstGraphQLError.extensions;
if (isObject(extensions) && extensions.code !== undefined) {
return String(extensions.code);
}
}
}

if (e.code !== undefined) {
return String(e.code);
}

if (typeof e.name === 'string' && e.name) {
const message = typeof e.message === 'string' ? e.message : '';
if (e.name !== 'Error') {
return message ? `${e.name}: ${message}` : e.name;
}
if (message) {
return message;
}
}

return undefined;
}

export enum TeamReviewerRefreshKind {
None,
Try,
Expand Down
68 changes: 68 additions & 0 deletions src/github/loggingOctokit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,52 @@ interface RateLimitResult {
} | undefined;
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

export function getErrorCode(e: unknown): string | undefined {
if (!isObject(e)) {
return undefined;
}

if (e.status !== undefined) {
return String(e.status);
}

const networkError = e.networkError;
if (isObject(networkError) && networkError.statusCode !== undefined) {
return String(networkError.statusCode);
}

const graphQLErrors = e.graphQLErrors;
if (Array.isArray(graphQLErrors)) {
const firstGraphQLError = graphQLErrors[0];
if (isObject(firstGraphQLError)) {
const extensions = firstGraphQLError.extensions;
if (isObject(extensions) && extensions.code !== undefined) {
return String(extensions.code);
}
}
}

if (e.code !== undefined) {
return String(e.code);
}

if (typeof e.name === 'string' && e.name) {
const message = typeof e.message === 'string' ? e.message : '';
if (e.name !== 'Error') {
return message ? `${e.name}: ${message}` : e.name;
}
if (message) {
return message;
}
Comment on lines +65 to +71
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getErrorCode falls back to returning Error name+message (or just message) when no status/code fields are present. Since this value is sent as the errorCode telemetry property (SystemMetaData) for pr.apiCallFailed and other events, including message can introduce high-cardinality values and may inadvertently capture sensitive data embedded in error messages. Consider restricting this to low-cardinality identifiers only (e.g., HTTP status/statusCode, GraphQL extensions.code, known code values, and possibly name without message), and return undefined/UnknownError otherwise.

Suggested change
const message = typeof e.message === 'string' ? e.message : '';
if (e.name !== 'Error') {
return message ? `${e.name}: ${message}` : e.name;
}
if (message) {
return message;
}
return e.name;

Copilot uses AI. Check for mistakes.
}

return undefined;
}

export class RateLogger {
private bulkhead: BulkheadPolicy = bulkhead(140);
private static ID = 'RateLimit';
Expand Down Expand Up @@ -111,6 +157,25 @@ export class RateLogger {
}
}

public logApiError(info: string | undefined, apiResult: Promise<unknown>): void {
apiResult.catch(e => {
const properties: { operation: string; errorCode?: string } = {
operation: RateLogger.sanitizeOperationName(info ?? 'unknown'),
};
const errorCode = getErrorCode(e);
if (errorCode) {
properties.errorCode = errorCode;
}
/* __GDPR__
"pr.apiCallFailed" : {
"operation": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"errorCode": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
}
*/
this.telemetry.sendTelemetryErrorEvent('pr.apiCallFailed', properties);
});
}

public async logRestRateLimit(info: string | undefined, restResponse: Promise<RestResponse>) {
let result;
try {
Expand Down Expand Up @@ -139,6 +204,7 @@ export class LoggingApolloClient {
throw new Error('API call count has exceeded a rate limit.');
}
this._rateLogger.logRateLimit(logInfo, result as Promise<RateLimitResult>);
this._rateLogger.logApiError(logInfo, result);
return result;
}

Expand All @@ -149,6 +215,7 @@ export class LoggingApolloClient {
throw new Error('API call count has exceeded a rate limit.');
}
this._rateLogger.logRateLimit(logInfo, result as Promise<RateLimitResult>);
this._rateLogger.logApiError(logInfo, result);
return result;
}
}
Expand All @@ -163,6 +230,7 @@ export class LoggingOctokit {
throw new Error('API call count has exceeded a rate limit.');
}
this._rateLogger.logRestRateLimit(logInfo, result as Promise<unknown> as Promise<RestResponse>);
this._rateLogger.logApiError(logInfo, result);
return result;
}
}
Expand Down
Loading