mirror of
https://github.com/LukeHagar/usage-statistics.git
synced 2025-12-09 04:22:01 +00:00
chore: update dependencies to include @octokit/plugin-retry and @octokit/plugin-throttling; refactor GitHubTracker to utilize these plugins for improved rate limiting and error handling
This commit is contained in:
18
bun.lock
18
bun.lock
@@ -5,6 +5,8 @@
|
|||||||
"name": "usage-statistics",
|
"name": "usage-statistics",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
|
"@octokit/plugin-retry": "^7.0.0",
|
||||||
|
"@octokit/plugin-throttling": "^7.0.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -41,9 +43,13 @@
|
|||||||
|
|
||||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-retry": ["@octokit/plugin-retry@7.2.1", "", { "dependencies": { "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-wUc3gv0D6vNHpGxSaR3FlqJpTXGWgqmk607N9L3LvPL4QjaxDgX/1nY2mGpT37Khn+nlIXdljczkRnNdTTV3/A=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-throttling": ["@octokit/plugin-throttling@7.0.0", "", { "dependencies": { "@octokit/types": "^11.0.0", "bottleneck": "^2.15.3" }, "peerDependencies": { "@octokit/core": "^5.0.0" } }, "sha512-KL2k/d0uANc8XqP5S64YcNFCudR3F5AaKO39XWdUtlJIjT9Ni79ekWJ6Kj5xvAw87udkOMEPcVf9xEge2+ahew=="],
|
||||||
|
|
||||||
"@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
"@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||||
|
|
||||||
"@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
|
||||||
|
|
||||||
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||||
|
|
||||||
@@ -55,6 +61,8 @@
|
|||||||
|
|
||||||
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||||
|
|
||||||
|
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
@@ -70,5 +78,13 @@
|
|||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||||
|
|
||||||
|
"@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-throttling/@octokit/types": ["@octokit/types@11.1.0", "", { "dependencies": { "@octokit/openapi-types": "^18.0.0" } }, "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ=="],
|
||||||
|
|
||||||
|
"@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||||
|
|
||||||
|
"@octokit/plugin-throttling/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@18.1.1", "", {}, "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
98
dist/action.js
vendored
98
dist/action.js
vendored
File diff suppressed because one or more lines are too long
@@ -24,7 +24,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@octokit/rest": "22.0.0"
|
"@octokit/rest": "22.0.0",
|
||||||
|
"@octokit/plugin-retry": "^7.0.0",
|
||||||
|
"@octokit/plugin-throttling": "^7.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.0.0"
|
"bun": ">=1.0.0"
|
||||||
|
|||||||
@@ -29,7 +29,14 @@ async function run() {
|
|||||||
// Set environment variables
|
// Set environment variables
|
||||||
if (githubToken) {
|
if (githubToken) {
|
||||||
process.env.GITHUB_TOKEN = githubToken;
|
process.env.GITHUB_TOKEN = githubToken;
|
||||||
|
core.info('✅ Using custom GitHub token for authentication');
|
||||||
|
} else if (process.env.GITHUB_TOKEN) {
|
||||||
|
// Use the default GitHub token if no custom token provided
|
||||||
|
core.info('✅ Using default GitHub token from environment');
|
||||||
|
} else {
|
||||||
|
core.warning('⚠️ No GitHub token provided. Some API calls may be rate limited.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postmanApiKey) {
|
if (postmanApiKey) {
|
||||||
process.env.POSTMAN_API_KEY = postmanApiKey;
|
process.env.POSTMAN_API_KEY = postmanApiKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Combines and analyzes statistics from all platform trackers
|
* Combines and analyzes statistics from all platform trackers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BaseDownloadStats, DownloadStatsAggregator, TrackingConfig } from './types';
|
import type { BaseDownloadStats, TrackingConfig } from './types';
|
||||||
import NpmTracker from './trackers/npm';
|
import NpmTracker from './trackers/npm';
|
||||||
import GitHubTracker from './trackers/github';
|
import GitHubTracker from './trackers/github';
|
||||||
import PyPiTracker from './trackers/pypi';
|
import PyPiTracker from './trackers/pypi';
|
||||||
@@ -11,7 +11,6 @@ import HomebrewTracker from './trackers/homebrew';
|
|||||||
import PowerShellTracker from './trackers/powershell';
|
import PowerShellTracker from './trackers/powershell';
|
||||||
import PostmanTracker from './trackers/postman';
|
import PostmanTracker from './trackers/postman';
|
||||||
import GoTracker from './trackers/go';
|
import GoTracker from './trackers/go';
|
||||||
import { globalRateLimiter } from './utils/rate-limiter';
|
|
||||||
|
|
||||||
export interface AggregatedStats {
|
export interface AggregatedStats {
|
||||||
totalDownloads: number;
|
totalDownloads: number;
|
||||||
@@ -29,7 +28,7 @@ export interface AggregatedStats {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DownloadStatsAggregator implements DownloadStatsAggregator {
|
export class DownloadStatsAggregator {
|
||||||
private trackers: Map<string, any> = new Map();
|
private trackers: Map<string, any> = new Map();
|
||||||
private config: TrackingConfig;
|
private config: TrackingConfig;
|
||||||
|
|
||||||
@@ -113,136 +112,122 @@ export class DownloadStatsAggregator implements DownloadStatsAggregator {
|
|||||||
async collectAllStats(): Promise<BaseDownloadStats[]> {
|
async collectAllStats(): Promise<BaseDownloadStats[]> {
|
||||||
const allStats: BaseDownloadStats[] = [];
|
const allStats: BaseDownloadStats[] = [];
|
||||||
|
|
||||||
// Collect NPM stats with rate limiting
|
// Collect NPM stats
|
||||||
if (this.config.npmPackages) {
|
if (this.config.npmPackages) {
|
||||||
const npmOperations = this.config.npmPackages.map(packageName =>
|
const npmPromises = this.config.npmPackages.map(async packageName => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const npmTracker = this.trackers.get('npm');
|
||||||
const npmTracker = this.trackers.get('npm');
|
const stats = await npmTracker.getDownloadStats(packageName);
|
||||||
const stats = await npmTracker.getDownloadStats(packageName);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting NPM stats for ${packageName}:`, error);
|
||||||
console.error(`Error collecting NPM stats for ${packageName}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const npmResults = await globalRateLimiter.throttleRequests(npmOperations, 2, 2000);
|
const npmResults = await Promise.all(npmPromises);
|
||||||
npmResults.forEach(stats => allStats.push(...stats));
|
npmResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect GitHub stats with rate limiting
|
// Collect GitHub stats (Octokit plugins handle rate limiting)
|
||||||
if (this.config.githubRepos) {
|
if (this.config.githubRepos) {
|
||||||
const githubOperations = this.config.githubRepos.map(repo =>
|
const githubPromises = this.config.githubRepos.map(async repo => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const githubTracker = this.trackers.get('github');
|
||||||
const githubTracker = this.trackers.get('github');
|
const stats = await githubTracker.getDownloadStats(repo);
|
||||||
const stats = await githubTracker.getDownloadStats(repo);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting GitHub stats for ${repo}:`, error);
|
||||||
console.error(`Error collecting GitHub stats for ${repo}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const githubResults = await globalRateLimiter.throttleRequests(githubOperations, 1, 3000);
|
const githubResults = await Promise.all(githubPromises);
|
||||||
githubResults.forEach(stats => allStats.push(...stats));
|
githubResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect PyPI stats with rate limiting
|
// Collect PyPI stats
|
||||||
if (this.config.pythonPackages) {
|
if (this.config.pythonPackages) {
|
||||||
const pypiOperations = this.config.pythonPackages.map(packageName =>
|
const pypiPromises = this.config.pythonPackages.map(async packageName => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const pypiTracker = this.trackers.get('pypi');
|
||||||
const pypiTracker = this.trackers.get('pypi');
|
const stats = await pypiTracker.getDownloadStats(packageName);
|
||||||
const stats = await pypiTracker.getDownloadStats(packageName);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting PyPI stats for ${packageName}:`, error);
|
||||||
console.error(`Error collecting PyPI stats for ${packageName}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const pypiResults = await globalRateLimiter.throttleRequests(pypiOperations, 2, 1500);
|
const pypiResults = await Promise.all(pypiPromises);
|
||||||
pypiResults.forEach(stats => allStats.push(...stats));
|
pypiResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect Homebrew stats with rate limiting
|
// Collect Homebrew stats
|
||||||
if (this.config.homebrewPackages) {
|
if (this.config.homebrewPackages) {
|
||||||
const homebrewOperations = this.config.homebrewPackages.map(packageName =>
|
const homebrewPromises = this.config.homebrewPackages.map(async packageName => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const homebrewTracker = this.trackers.get('homebrew');
|
||||||
const homebrewTracker = this.trackers.get('homebrew');
|
const stats = await homebrewTracker.getDownloadStats(packageName);
|
||||||
const stats = await homebrewTracker.getDownloadStats(packageName);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting Homebrew stats for ${packageName}:`, error);
|
||||||
console.error(`Error collecting Homebrew stats for ${packageName}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const homebrewResults = await globalRateLimiter.throttleRequests(homebrewOperations, 2, 2000);
|
const homebrewResults = await Promise.all(homebrewPromises);
|
||||||
homebrewResults.forEach(stats => allStats.push(...stats));
|
homebrewResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect PowerShell stats with rate limiting
|
// Collect PowerShell stats
|
||||||
if (this.config.powershellModules) {
|
if (this.config.powershellModules) {
|
||||||
const powershellOperations = this.config.powershellModules.map(moduleName =>
|
const powershellPromises = this.config.powershellModules.map(async moduleName => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const powershellTracker = this.trackers.get('powershell');
|
||||||
const powershellTracker = this.trackers.get('powershell');
|
const stats = await powershellTracker.getDownloadStats(moduleName);
|
||||||
const stats = await powershellTracker.getDownloadStats(moduleName);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting PowerShell stats for ${moduleName}:`, error);
|
||||||
console.error(`Error collecting PowerShell stats for ${moduleName}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const powershellResults = await globalRateLimiter.throttleRequests(powershellOperations, 2, 2000);
|
const powershellResults = await Promise.all(powershellPromises);
|
||||||
powershellResults.forEach(stats => allStats.push(...stats));
|
powershellResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect Postman stats with rate limiting
|
// Collect Postman stats
|
||||||
if (this.config.postmanCollections) {
|
if (this.config.postmanCollections) {
|
||||||
const postmanOperations = this.config.postmanCollections.map(collectionId =>
|
const postmanPromises = this.config.postmanCollections.map(async collectionId => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const postmanTracker = this.trackers.get('postman');
|
||||||
const postmanTracker = this.trackers.get('postman');
|
const stats = await postmanTracker.getDownloadStats(collectionId);
|
||||||
const stats = await postmanTracker.getDownloadStats(collectionId);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting Postman stats for ${collectionId}:`, error);
|
||||||
console.error(`Error collecting Postman stats for ${collectionId}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const postmanResults = await globalRateLimiter.throttleRequests(postmanOperations, 2, 2000);
|
const postmanResults = await Promise.all(postmanPromises);
|
||||||
postmanResults.forEach(stats => allStats.push(...stats));
|
postmanResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect Go stats with rate limiting
|
// Collect Go stats
|
||||||
if (this.config.goModules) {
|
if (this.config.goModules) {
|
||||||
const goOperations = this.config.goModules.map(moduleName =>
|
const goPromises = this.config.goModules.map(async moduleName => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const goTracker = this.trackers.get('go');
|
||||||
const goTracker = this.trackers.get('go');
|
const stats = await goTracker.getDownloadStats(moduleName);
|
||||||
const stats = await goTracker.getDownloadStats(moduleName);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting Go stats for ${moduleName}:`, error);
|
||||||
console.error(`Error collecting Go stats for ${moduleName}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const goResults = await globalRateLimiter.throttleRequests(goOperations, 2, 2000);
|
const goResults = await Promise.all(goPromises);
|
||||||
goResults.forEach(stats => allStats.push(...stats));
|
goResults.forEach(stats => allStats.push(...stats));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,19 +248,17 @@ export class DownloadStatsAggregator implements DownloadStatsAggregator {
|
|||||||
const allStats: BaseDownloadStats[] = [];
|
const allStats: BaseDownloadStats[] = [];
|
||||||
const packages = this.getPackagesForPlatform(platform);
|
const packages = this.getPackagesForPlatform(platform);
|
||||||
|
|
||||||
const operations = packages.map(packageName =>
|
const promises = packages.map(async packageName => {
|
||||||
async () => {
|
try {
|
||||||
try {
|
const stats = await tracker.getDownloadStats(packageName);
|
||||||
const stats = await tracker.getDownloadStats(packageName);
|
return stats;
|
||||||
return stats;
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(`Error collecting ${platform} stats for ${packageName}:`, error);
|
||||||
console.error(`Error collecting ${platform} stats for ${packageName}:`, error);
|
return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
const results = await globalRateLimiter.throttleRequests(operations, 2, 2000);
|
const results = await Promise.all(promises);
|
||||||
results.forEach(stats => allStats.push(...stats));
|
results.forEach(stats => allStats.push(...stats));
|
||||||
|
|
||||||
return allStats;
|
return allStats;
|
||||||
|
|||||||
52
src/github-tracker.test.ts
Normal file
52
src/github-tracker.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "bun:test";
|
||||||
|
import GitHubTracker from "./trackers/github";
|
||||||
|
|
||||||
|
describe("GitHubTracker", () => {
|
||||||
|
let tracker: GitHubTracker;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Use a test token or no token for testing
|
||||||
|
tracker = new GitHubTracker(process.env.GITHUB_TOKEN || undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Rate Limiting", () => {
|
||||||
|
it("should handle rate limiting gracefully", async () => {
|
||||||
|
// Test with a popular repository that might hit rate limits
|
||||||
|
const stats = await tracker.getDownloadStats('microsoft/vscode');
|
||||||
|
|
||||||
|
// Should return an array (even if empty due to rate limiting)
|
||||||
|
expect(Array.isArray(stats)).toBe(true);
|
||||||
|
}, 15000); // 15 second timeout for rate limit handling
|
||||||
|
|
||||||
|
it("should get package info", async () => {
|
||||||
|
try {
|
||||||
|
const info = await tracker.getPackageInfo('microsoft/vscode');
|
||||||
|
expect(info).toBeDefined();
|
||||||
|
expect(info.name).toBe('vscode');
|
||||||
|
expect(info.full_name).toBe('microsoft/vscode');
|
||||||
|
} catch (error) {
|
||||||
|
// If rate limited, that's expected behavior
|
||||||
|
console.log('Rate limited during test (expected):', error);
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
it("should get latest version", async () => {
|
||||||
|
try {
|
||||||
|
const version = await tracker.getLatestVersion('microsoft/vscode');
|
||||||
|
// Should return a version string or null
|
||||||
|
expect(version === null || typeof version === 'string').toBe(true);
|
||||||
|
} catch (error) {
|
||||||
|
// If rate limited, that's expected behavior
|
||||||
|
console.log('Rate limited during test (expected):', error);
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Configuration", () => {
|
||||||
|
it("should have correct name", () => {
|
||||||
|
expect(tracker.name).toBe('github');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,15 +7,15 @@ describe("UsageStatisticsManager", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const config: TrackingConfig = {
|
const config: TrackingConfig = {
|
||||||
enableLogging: true,
|
enableLogging: false, // Disable logging for tests
|
||||||
updateInterval: 60 * 60 * 1000,
|
updateInterval: 60 * 60 * 1000,
|
||||||
npmPackages: ['lodash', 'axios'],
|
npmPackages: ['lodash'], // Reduce to single package for faster tests
|
||||||
githubRepos: ['microsoft/vscode', 'facebook/react'],
|
githubRepos: ['microsoft/vscode'], // Reduce to single repo
|
||||||
pythonPackages: ['requests', 'numpy'],
|
pythonPackages: ['requests'], // Reduce to single package
|
||||||
homebrewPackages: ['git', 'node'],
|
homebrewPackages: ['git'], // Reduce to single package
|
||||||
powershellModules: ['PowerShellGet'],
|
powershellModules: ['PowerShellGet'],
|
||||||
postmanCollections: [],
|
postmanCollections: [],
|
||||||
goModules: ['github.com/gin-gonic/gin', 'github.com/go-chi/chi']
|
goModules: ['github.com/go-chi/chi'] // Reduce to single module
|
||||||
};
|
};
|
||||||
manager = new UsageStatisticsManager(config);
|
manager = new UsageStatisticsManager(config);
|
||||||
});
|
});
|
||||||
@@ -31,7 +31,7 @@ describe("UsageStatisticsManager", () => {
|
|||||||
expect(Array.isArray(report.topPackages)).toBe(true);
|
expect(Array.isArray(report.topPackages)).toBe(true);
|
||||||
expect(report.topPackages.length).toBeGreaterThan(0);
|
expect(report.topPackages.length).toBeGreaterThan(0);
|
||||||
expect(typeof report.platformBreakdown).toBe('object');
|
expect(typeof report.platformBreakdown).toBe('object');
|
||||||
}, 30000); // 30 second timeout
|
}, 10000); // 10 second timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPlatformReport", () => {
|
describe("getPlatformReport", () => {
|
||||||
@@ -54,7 +54,7 @@ describe("UsageStatisticsManager", () => {
|
|||||||
// Should be valid JSON
|
// Should be valid JSON
|
||||||
const parsed = JSON.parse(jsonReport);
|
const parsed = JSON.parse(jsonReport);
|
||||||
expect(parsed).toBeDefined();
|
expect(parsed).toBeDefined();
|
||||||
}, 30000); // 30 second timeout
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
it("should export CSV report", async () => {
|
it("should export CSV report", async () => {
|
||||||
const csvReport = await manager.exportReport('csv');
|
const csvReport = await manager.exportReport('csv');
|
||||||
@@ -62,7 +62,7 @@ describe("UsageStatisticsManager", () => {
|
|||||||
expect(csvReport).toBeDefined();
|
expect(csvReport).toBeDefined();
|
||||||
expect(typeof csvReport).toBe('string');
|
expect(typeof csvReport).toBe('string');
|
||||||
expect(csvReport.includes('Platform,Package,Downloads')).toBe(true);
|
expect(csvReport.includes('Platform,Package,Downloads')).toBe(true);
|
||||||
}, 30000); // 30 second timeout
|
}, 10000); // 10 second timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getLastUpdateTime", () => {
|
describe("getLastUpdateTime", () => {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import type { BaseDownloadStats, PlatformTracker } from '../types';
|
import type { BaseDownloadStats, PlatformTracker } from '../types';
|
||||||
import { Octokit } from '@octokit/rest';
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import { retry } from '@octokit/plugin-retry';
|
||||||
|
import { throttling } from '@octokit/plugin-throttling';
|
||||||
|
|
||||||
export interface GitHubDownloadStats extends BaseDownloadStats {
|
export interface GitHubDownloadStats extends BaseDownloadStats {
|
||||||
platform: 'github';
|
platform: 'github';
|
||||||
@@ -57,30 +59,35 @@ export class GitHubTracker implements PlatformTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initializeOctokit() {
|
private async initializeOctokit() {
|
||||||
if (typeof window === 'undefined') {
|
// Create Octokit with retry and throttling plugins
|
||||||
// Server-side: use Octokit
|
const MyOctokit = Octokit.plugin(retry, throttling);
|
||||||
this.octokit = new Octokit({
|
|
||||||
auth: this.token,
|
this.octokit = new MyOctokit({
|
||||||
userAgent: 'usage-statistics-tracker',
|
auth: this.token,
|
||||||
timeZone: 'UTC',
|
userAgent: 'usage-statistics-tracker',
|
||||||
baseUrl: 'https://api.github.com',
|
timeZone: 'UTC',
|
||||||
log: {
|
baseUrl: 'https://api.github.com',
|
||||||
debug: () => {},
|
log: {
|
||||||
info: () => {},
|
debug: () => {},
|
||||||
warn: console.warn,
|
info: () => {},
|
||||||
error: console.error
|
warn: console.warn,
|
||||||
|
error: console.error
|
||||||
|
},
|
||||||
|
throttle: {
|
||||||
|
onRateLimit: (retryAfter: number, options: any) => {
|
||||||
|
console.warn(`Rate limit hit for ${options.request.url}, retrying after ${retryAfter} seconds`);
|
||||||
|
return true; // Retry after the specified time
|
||||||
},
|
},
|
||||||
request: {
|
onSecondaryRateLimit: (retryAfter: number, options: any) => {
|
||||||
timeout: 10000,
|
console.warn(`Secondary rate limit hit for ${options.request.url}, retrying after ${retryAfter} seconds`);
|
||||||
retries: 3,
|
return true; // Retry after the specified time
|
||||||
retryAfterBaseValue: 1,
|
|
||||||
retryAfterMaxValue: 60
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
} else {
|
retry: {
|
||||||
// Client-side: use fetch API
|
doNotRetry: [400, 401, 403, 404, 422], // Don't retry on these status codes
|
||||||
this.octokit = null;
|
enabled: true
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDownloadStats(repository: string, options?: {
|
async getDownloadStats(repository: string, options?: {
|
||||||
@@ -154,91 +161,41 @@ export class GitHubTracker implements PlatformTracker {
|
|||||||
async getPackageInfo(repository: string): Promise<GitHubRepositoryInfo> {
|
async getPackageInfo(repository: string): Promise<GitHubRepositoryInfo> {
|
||||||
const [owner, repo] = repository.split('/');
|
const [owner, repo] = repository.split('/');
|
||||||
|
|
||||||
if (this.octokit) {
|
if (!this.octokit) {
|
||||||
// Use Octokit if available
|
throw new Error('Octokit not initialized');
|
||||||
try {
|
}
|
||||||
const response = await this.octokit.repos.get({
|
|
||||||
owner,
|
try {
|
||||||
repo
|
const response = await this.octokit.repos.get({
|
||||||
});
|
owner,
|
||||||
return response.data;
|
repo
|
||||||
} catch (error: any) {
|
|
||||||
if (error.status === 403 && error.message.includes('abuse detection')) {
|
|
||||||
console.warn(`Rate limit hit for ${repository}, waiting 60 seconds...`);
|
|
||||||
await this.sleep(60000);
|
|
||||||
return this.getPackageInfo(repository); // Retry once
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to fetch API
|
|
||||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
||||||
headers: this.token ? {
|
|
||||||
'Authorization': `token ${this.token}`,
|
|
||||||
'Accept': 'application/vnd.github.v3+json'
|
|
||||||
} : {
|
|
||||||
'Accept': 'application/vnd.github.v3+json'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return response.data;
|
||||||
if (!response.ok) {
|
} catch (error: any) {
|
||||||
if (response.status === 403) {
|
console.error(`Error fetching repository info for ${repository}:`, error);
|
||||||
console.warn(`Rate limit hit for ${repository}, waiting 60 seconds...`);
|
throw error;
|
||||||
await this.sleep(60000);
|
|
||||||
return this.getPackageInfo(repository); // Retry once
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch repository info for ${repository}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getReleases(owner: string, repo: string): Promise<GitHubReleaseInfo[]> {
|
private async getReleases(owner: string, repo: string): Promise<GitHubReleaseInfo[]> {
|
||||||
if (this.octokit) {
|
if (!this.octokit) {
|
||||||
// Use Octokit if available
|
throw new Error('Octokit not initialized');
|
||||||
try {
|
}
|
||||||
const response = await this.octokit.repos.listReleases({
|
|
||||||
owner,
|
try {
|
||||||
repo,
|
const response = await this.octokit.repos.listReleases({
|
||||||
per_page: 100
|
owner,
|
||||||
});
|
repo,
|
||||||
return response.data;
|
per_page: 100
|
||||||
} catch (error: any) {
|
|
||||||
if (error.status === 403 && error.message.includes('abuse detection')) {
|
|
||||||
console.warn(`Rate limit hit for ${owner}/${repo}, waiting 60 seconds...`);
|
|
||||||
await this.sleep(60000);
|
|
||||||
return this.getReleases(owner, repo); // Retry once
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback to fetch API
|
|
||||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`, {
|
|
||||||
headers: this.token ? {
|
|
||||||
'Authorization': `token ${this.token}`,
|
|
||||||
'Accept': 'application/vnd.github.v3+json'
|
|
||||||
} : {
|
|
||||||
'Accept': 'application/vnd.github.v3+json'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return response.data;
|
||||||
if (!response.ok) {
|
} catch (error: any) {
|
||||||
if (response.status === 403) {
|
console.error(`Error fetching releases for ${owner}/${repo}:`, error);
|
||||||
console.warn(`Rate limit hit for ${owner}/${repo}, waiting 60 seconds...`);
|
throw error;
|
||||||
await this.sleep(60000);
|
|
||||||
return this.getReleases(owner, repo); // Retry once
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch releases for ${owner}/${repo}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GitHubTracker;
|
export default GitHubTracker;
|
||||||
@@ -17,14 +17,6 @@ export interface PlatformTracker {
|
|||||||
getPackageInfo(packageName: string): Promise<any>;
|
getPackageInfo(packageName: string): Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadStatsAggregator {
|
|
||||||
aggregateStats(stats: BaseDownloadStats[]): {
|
|
||||||
totalDownloads: number;
|
|
||||||
uniquePackages: number;
|
|
||||||
platforms: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackingConfig {
|
export interface TrackingConfig {
|
||||||
npmPackages?: string[];
|
npmPackages?: string[];
|
||||||
goModules?: string[];
|
goModules?: string[];
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
/**
|
|
||||||
* Rate Limiting Utility
|
|
||||||
* Handles API rate limits with exponential backoff and proper error handling
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface RateLimitConfig {
|
|
||||||
maxRetries: number;
|
|
||||||
baseDelay: number;
|
|
||||||
maxDelay: number;
|
|
||||||
backoffMultiplier: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RateLimiter {
|
|
||||||
private config: RateLimitConfig;
|
|
||||||
private requestCounts: Map<string, { count: number; resetTime: number }> = new Map();
|
|
||||||
|
|
||||||
constructor(config: Partial<RateLimitConfig> = {}) {
|
|
||||||
this.config = {
|
|
||||||
maxRetries: config.maxRetries || 3,
|
|
||||||
baseDelay: config.baseDelay || 1000,
|
|
||||||
maxDelay: config.maxDelay || 60000,
|
|
||||||
backoffMultiplier: config.backoffMultiplier || 2
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeWithRetry<T>(
|
|
||||||
operation: () => Promise<T>,
|
|
||||||
operationName: string,
|
|
||||||
retryCount = 0
|
|
||||||
): Promise<T> {
|
|
||||||
try {
|
|
||||||
return await operation();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (this.shouldRetry(error) && retryCount < this.config.maxRetries) {
|
|
||||||
const delay = this.calculateDelay(retryCount);
|
|
||||||
console.warn(`Rate limit hit for ${operationName}, retrying in ${delay}ms (attempt ${retryCount + 1}/${this.config.maxRetries})`);
|
|
||||||
|
|
||||||
await this.sleep(delay);
|
|
||||||
return this.executeWithRetry(operation, operationName, retryCount + 1);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldRetry(error: any): boolean {
|
|
||||||
// Check for rate limiting errors
|
|
||||||
if (error.status === 403 || error.status === 429) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for abuse detection
|
|
||||||
if (error.message && error.message.includes('abuse detection')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for rate limit headers
|
|
||||||
if (error.headers && (error.headers['x-ratelimit-remaining'] === '0' || error.headers['retry-after'])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateDelay(retryCount: number): number {
|
|
||||||
const delay = this.config.baseDelay * Math.pow(this.config.backoffMultiplier, retryCount);
|
|
||||||
return Math.min(delay, this.config.maxDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async throttleRequests<T>(
|
|
||||||
operations: Array<() => Promise<T>>,
|
|
||||||
maxConcurrent = 3,
|
|
||||||
delayBetweenRequests = 1000
|
|
||||||
): Promise<T[]> {
|
|
||||||
const results: T[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < operations.length; i += maxConcurrent) {
|
|
||||||
const batch = operations.slice(i, i + maxConcurrent);
|
|
||||||
const batchPromises = batch.map(async (operation, index) => {
|
|
||||||
// Add delay between requests in the same batch
|
|
||||||
if (index > 0) {
|
|
||||||
await this.sleep(delayBetweenRequests);
|
|
||||||
}
|
|
||||||
return this.executeWithRetry(operation, `batch-${i}-${index}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const batchResults = await Promise.all(batchPromises);
|
|
||||||
results.push(...batchResults);
|
|
||||||
|
|
||||||
// Add delay between batches
|
|
||||||
if (i + maxConcurrent < operations.length) {
|
|
||||||
await this.sleep(delayBetweenRequests * 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track rate limits for different APIs
|
|
||||||
trackRequest(apiName: string, resetTime?: number): void {
|
|
||||||
const now = Date.now();
|
|
||||||
const current = this.requestCounts.get(apiName) || { count: 0, resetTime: now + 3600000 }; // Default 1 hour
|
|
||||||
|
|
||||||
current.count++;
|
|
||||||
|
|
||||||
if (resetTime) {
|
|
||||||
current.resetTime = resetTime * 1000; // Convert to milliseconds
|
|
||||||
}
|
|
||||||
|
|
||||||
this.requestCounts.set(apiName, current);
|
|
||||||
}
|
|
||||||
|
|
||||||
isRateLimited(apiName: string): boolean {
|
|
||||||
const current = this.requestCounts.get(apiName);
|
|
||||||
if (!current) return false;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (now > current.resetTime) {
|
|
||||||
this.requestCounts.delete(apiName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conservative estimate - assume we're rate limited if we've made many requests recently
|
|
||||||
return current.count > 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRemainingTime(apiName: string): number {
|
|
||||||
const current = this.requestCounts.get(apiName);
|
|
||||||
if (!current) return 0;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
return Math.max(0, current.resetTime - now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global rate limiter instance
|
|
||||||
export const globalRateLimiter = new RateLimiter();
|
|
||||||
Reference in New Issue
Block a user