Skip to content

Respect FACEIT developer usage: fewer API calls, bounded cache, clearer errors - #34

Merged
m9tzin merged 4 commits into
mainfrom
cursor/faceit-rules-fixes-33a9
Apr 26, 2026
Merged

Respect FACEIT developer usage: fewer API calls, bounded cache, clearer errors#34
m9tzin merged 4 commits into
mainfrom
cursor/faceit-rules-fixes-33a9

Conversation

@m9tzin

@m9tzin m9tzin commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

This change aligns the service with responsible use of the FACEIT Data API (single authenticated request per player lookup, reduced burst traffic, no non-FACEIT side channel for match data).

Changes

  • Player resolution: One GET /players?nickname= per request, with nickname normalized to lowercase, instead of up to four parallel calls.
  • Match stats: Fetches /matches/{id}/stats with a small concurrency cap (5) instead of 30 simultaneous calls per /stats request; 404 on stats returns null; other HTTP errors surface as structured FaceitApiError.
  • Errors: Introduced FaceitApiError and NoCS2DataError; removed broad err.message.includes('CS2') handling. Twitch-facing routes still return HTTP 200 for bot compatibility, including a clear message when FACEIT returns 429.
  • Cache: LRU eviction and optional CACHE_MAX_ENTRIES (default 500); expired entries are removed on read.
  • Correctness: Streak no longer treats missing team mapping as a loss (? instead); HS% uses Headshots % when parseable, else Headshots/Kills; winrate counts only matches where the player appears on a team.
  • Removed: Unused third-party fls-api.vercel.app integration and unused getMatchDetails helper.
  • Tests / CI: npm test previously always exited 1. Replaced with Node's built-in node --test suite (test/cache.test.js, test/faceit-streak.test.js) and a CI step that runs npm test before the app start check.

Configuration

Variable Description
CACHE_MAX_ENTRIES Optional. Max distinct cache keys (default 500, minimum 50).

Testing

  • npm test (node:test)
  • node --check on modified modules
  • timeout 3s npm start with FACEIT_KEY / PORT set
Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • New Features

    • Configurable cache entry limits via environment variable (CACHE_MAX_ENTRIES)
    • Enhanced error handling with clearer, localized error messages
  • Improvements

    • Implemented automatic least-recently-used cache eviction for better memory management
    • Improved player data accuracy with nickname normalization
    • Optimized concurrent match statistics processing for better performance
  • Tests

    • Added test coverage for cache functionality and match streak processing
    • Enabled automated test execution in CI pipeline

…errors

- One Data API call per player resolve (lowercase nickname) instead of
  parallel variant fan-out to reduce rate-limit risk.
- Structured FaceitApiError with Twitch-friendly 200 responses for 429/5xx;
  NoCS2DataError instead of string-matching CS2 messages.
- LRU-bounded response cache (CACHE_MAX_ENTRIES) with TTL eviction on read.
- Match stats fetched with limited concurrency; 404 stats return null;
  other API errors propagate. Removed unused third-party FLS fetch.
- Streak skips unknown team as ?; HS% from field or Headshots/Kills fallback;
  winrate uses matches where the player appears on a team.

Co-authored-by: Matheus Marinho <m9tzin@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cursor[bot] has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 51 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f41b4511-513a-4ad7-b373-edb12a78fb86

📥 Commits

Reviewing files that changed from the base of the PR and between d6da294 and 7bbd025.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • package.json
📝 Walkthrough

Walkthrough

The PR introduces improved cache management with size limits and LRU eviction, adds specialized error classes for better error handling, refactors faceitService with bounded concurrency for match stats fetching and enhanced stats computation, updates routes to use the new cache API, and establishes test infrastructure with new test files for cache and match streak functionality.

Changes

Cohort / File(s) Summary
Test Infrastructure
.github/workflows/ci.yml, package.json
Adds npm test step to CI workflow and configures npm test script to execute Node.js test runner against test/ directory.
Cache System
src/config/index.js, src/utils/cache.js
Introduces maxEntries config parameter; rewrites cache from plain object to Map-based implementation with LRU eviction, updated set signature to accept maxEntries parameter, and improved TTL/access tracking.
Error Handling
src/middlewares/errorHandler.js
Adds new NoCS2DataError and FaceitApiError classes; refactors error handler to branch on error types and return HTTP 200 with Portuguese messages for specific FACEIT failure scenarios.
Route Updates
src/routes/elo.js, src/routes/stats.js, src/routes/streak.js
Updates all routes to use new NoCS2DataError and pass config.cache.maxEntries to cache.set calls; removes generic error messages in favor of specialized error classes.
Service Layer
src/services/faceitService.js
Introduces FaceitApiError class; refactors getPlayerData to normalize lowercase nicknames and perform single FACEIT lookup; adds bounded concurrency (mapPool) for match stats fetching; revises last-30 stats headshot computation with fallback logic; updates processMatchStreak to handle missing player team with '?' marker.
Test Suites
test/cache.test.js, test/faceit-streak.test.js
Introduces cache utility tests verifying TTL behavior and LRU eviction; adds match streak tests for processMatchStreak with missing player id scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • m9tzin/faceit-elo-bot#26: Both PRs modify winrate calculation logic in calculateLast30MatchesStats to handle per-match stats edge cases and update denominator computation.

Suggested labels

enhancement

Poem

🐰 Hops with glee!
A cache that shrinks with LRU care,
Errors now speak with class so fair,
Tests confirm the streaks are square—
Concurrency bounded, stats laid bare! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the three main objectives: reducing FACEIT API calls (via single player lookup and bounded concurrency), implementing bounded cache with LRU eviction, and introducing clearer error types.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/faceit-rules-fixes-33a9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

cursoragent and others added 2 commits April 26, 2026 22:02
Reproduced: npm test always exited 1 ('no test specified'). Add cache LRU/TTL
and processMatchStreak regression tests; run npm test in CI before start check.

Co-authored-by: Matheus Marinho <m9tzin@users.noreply.github.com>
…IGTERM

The previous script used 'timeout ... || code=0' so success paths never set
code; combined with Node exiting 0 after SIGTERM, CI could fail incorrectly.
Always capture timeout's exit status and treat 0 and 124 as success.

Co-authored-by: Matheus Marinho <m9tzin@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
test/faceit-streak.test.js (1)

5-31: Consider adding a Loss case.

Both branches of the won ? 'W' : 'L' ternary should be exercised; currently only W and ? are covered.

♻️ Suggested addition
+  it('marks loss when player team does not match winner', () => {
+    const pid = 'me';
+    const match = {
+      teams: {
+        faction1: { players: [{ player_id: pid }] },
+        faction2: { players: [{ player_id: 'other' }] }
+      },
+      results: { winner: 'faction2' }
+    };
+    const out = processMatchStreak([match], pid);
+    assert.match(out, /L/);
+    assert.doesNotMatch(out, /\?/);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/faceit-streak.test.js` around lines 5 - 31, Add a test case exercising
the loss branch of processMatchStreak: create a match where the player is on one
faction (e.g., faction1 with player_id 'me') and results.winner is the opposite
faction (e.g., 'faction2'), call processMatchStreak([match], 'me') and assert
the output contains 'L' and does not contain '?'; place this alongside the
existing tests so both branches of the won ? 'W' : 'L' ternary are covered.
src/utils/cache.js (1)

15-26: Defensive guard for non-numeric maxEntries.

If maxEntries is undefined/NaN (e.g., a future caller forgets to thread config.cache.maxEntries, or CACHE_MAX_ENTRIES parsing yields NaN), this.store.size < maxEntries evaluates to false, so the function will evict an LRU entry on every insertion of a new key — silently degrading the cache to a single live entry. Validate the argument before iterating.

🛡️ Proposed guard
   evictLruIfNeeded(maxEntries) {
+    if (!Number.isFinite(maxEntries) || maxEntries <= 0) return;
     if (this.store.size < maxEntries) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/cache.js` around lines 15 - 26, The evictLruIfNeeded method should
defensively validate the maxEntries argument before using it: in
evictLruIfNeeded(maxEntries) check that maxEntries is a finite positive number
(e.g., Number.isFinite(maxEntries) && maxEntries > 0) and return early if not,
so the subsequent comparison this.store.size < maxEntries and the LRU eviction
loop are skipped when callers pass undefined/NaN or invalid values; update
evictLruIfNeeded to perform this guard before iterating over this.store.
src/services/faceitService.js (2)

138-153: Verify behavior when a mapper rejects in mapPool.

When one worker's mapper throws (e.g., a non-404 FaceitApiError from getMatchStats), Promise.all will reject, but other in-flight workers continue draining next until the queue empties. That's fine for correctness (no leaks) but means any subsequent rejections from sibling workers become unhandled rejections after the first failure has already settled the outer promise.

If you want to fail fast and stop scheduling more work on the first error, gate worker() on a shared aborted flag:

♻️ Optional fail-fast guard
 async function mapPool(items, concurrency, mapper) {
   const results = new Array(items.length);
   let next = 0;
+  let aborted = false;
   const n = Math.max(1, Math.min(concurrency, items.length));

   async function worker() {
     while (true) {
+      if (aborted) break;
       const idx = next++;
       if (idx >= items.length) break;
-      results[idx] = await mapper(items[idx], idx);
+      try {
+        results[idx] = await mapper(items[idx], idx);
+      } catch (err) {
+        aborted = true;
+        throw err;
+      }
     }
   }

   await Promise.all(Array.from({ length: n }, () => worker()));
   return results;
 }

In practice getMatchStats only rethrows non-404 FaceitApiErrors, so this is mostly a defensive nit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/faceitService.js` around lines 138 - 153, The mapPool worker
loop can continue scheduling tasks after one mapper rejects, causing later
rejections to become unhandled; modify mapPool to add a shared aborted flag
(e.g., let aborted = false) that each worker checks before taking the next index
and set aborted = true when a mapper throws, then rethrow the error so
Promise.all rejects; update the inner function worker() to bail out early if
aborted is true and avoid incrementing/consuming next when aborted so no further
work is scheduled after the first failure (refer to mapPool, worker, next, and
mapper in your changes).

309-322: Minor: deduplicate parseInt(stats['Kills'] || 0, 10).

Kills is parsed twice — once for totalKills and once for the HS% fallback. Folding the parses together makes intent clearer and avoids redoing the work.

♻️ Proposed refactor
-    totalKills += parseInt(stats['Kills'] || 0, 10);
-    totalDeaths += parseInt(stats['Deaths'] || 0, 10);
-
-    const hsPctField = parseHeadshotPercentField(stats['Headshots %']);
-    const kills = parseInt(stats['Kills'] || 0, 10);
-    const headshotKills = parseInt(stats['Headshots'] || 0, 10);
-    let pct = hsPctField;
+    const kills = parseInt(stats['Kills'] || 0, 10);
+    const deaths = parseInt(stats['Deaths'] || 0, 10);
+    totalKills += kills;
+    totalDeaths += deaths;
+
+    const hsPctField = parseHeadshotPercentField(stats['Headshots %']);
+    const headshotKills = parseInt(stats['Headshots'] || 0, 10);
+    let pct = hsPctField;
     if (pct == null && kills > 0) {
       pct = (headshotKills / kills) * 100;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/faceitService.js` around lines 309 - 322, The code parses
stats['Kills'] twice; parse it once into a local variable (e.g., const kills =
parseInt(stats['Kills'] || 0, 10)) and use that single kills value both to add
to totalKills and later when computing the headshot % fallback, leaving
totalDeaths parsing and parseHeadshotPercentField(stats['Headshots %'])
unchanged; update references to the previous kills/local variable and remove the
duplicate parse to avoid redundant work in the block handling
hsPercentSum/hsPercentCount and totalKills.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 9: The package.json engines.node declaration conflicts with the test
runner (node --test requires Node 18+), so update the "engines.node" field in
package.json (look for the engines.node key) to a minimum of ">=18.0.0" (or
">=20.0.0" if you want to require the stable runner) so the local npm test, CI
matrix (18.x/20.x/22.x) and engine declaration are consistent.

---

Nitpick comments:
In `@src/services/faceitService.js`:
- Around line 138-153: The mapPool worker loop can continue scheduling tasks
after one mapper rejects, causing later rejections to become unhandled; modify
mapPool to add a shared aborted flag (e.g., let aborted = false) that each
worker checks before taking the next index and set aborted = true when a mapper
throws, then rethrow the error so Promise.all rejects; update the inner function
worker() to bail out early if aborted is true and avoid incrementing/consuming
next when aborted so no further work is scheduled after the first failure (refer
to mapPool, worker, next, and mapper in your changes).
- Around line 309-322: The code parses stats['Kills'] twice; parse it once into
a local variable (e.g., const kills = parseInt(stats['Kills'] || 0, 10)) and use
that single kills value both to add to totalKills and later when computing the
headshot % fallback, leaving totalDeaths parsing and
parseHeadshotPercentField(stats['Headshots %']) unchanged; update references to
the previous kills/local variable and remove the duplicate parse to avoid
redundant work in the block handling hsPercentSum/hsPercentCount and totalKills.

In `@src/utils/cache.js`:
- Around line 15-26: The evictLruIfNeeded method should defensively validate the
maxEntries argument before using it: in evictLruIfNeeded(maxEntries) check that
maxEntries is a finite positive number (e.g., Number.isFinite(maxEntries) &&
maxEntries > 0) and return early if not, so the subsequent comparison
this.store.size < maxEntries and the LRU eviction loop are skipped when callers
pass undefined/NaN or invalid values; update evictLruIfNeeded to perform this
guard before iterating over this.store.

In `@test/faceit-streak.test.js`:
- Around line 5-31: Add a test case exercising the loss branch of
processMatchStreak: create a match where the player is on one faction (e.g.,
faction1 with player_id 'me') and results.winner is the opposite faction (e.g.,
'faction2'), call processMatchStreak([match], 'me') and assert the output
contains 'L' and does not contain '?'; place this alongside the existing tests
so both branches of the won ? 'W' : 'L' ternary are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1cb80ada-f0fd-4e97-9a57-b49a4772cd53

📥 Commits

Reviewing files that changed from the base of the PR and between c685050 and d6da294.

📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • package.json
  • src/config/index.js
  • src/middlewares/cache.js
  • src/middlewares/errorHandler.js
  • src/routes/elo.js
  • src/routes/stats.js
  • src/routes/streak.js
  • src/services/faceitService.js
  • src/utils/cache.js
  • test/cache.test.js
  • test/faceit-streak.test.js

Comment thread package.json Outdated
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test test/"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Which Node.js version stabilized the built-in test runner (node --test)?

💡 Result:

Node.js version 20.0.0 stabilized the built-in test runner (node --test).

Citations:


🏁 Script executed:

# Read package.json to see the actual engines field and context around line 24-26
cat -n package.json | head -30

Repository: m9tzin/faceit-elo-bot

Length of output: 917


🏁 Script executed:

# Search for CI configuration files
find . -name "*.yml" -o -name "*.yaml" | grep -E "(\.github|\.gitlab|\.circleci|ci)" | head -20

Repository: m9tzin/faceit-elo-bot

Length of output: 203


🏁 Script executed:

# Check the CI matrix in the ci.yml file
cat .github/workflows/ci.yml

Repository: m9tzin/faceit-elo-bot

Length of output: 3666


engines.node declaration does not match the test runner minimum version.

node --test requires Node 18+ (experimental) and is only stable from Node 20. The engines.node: ">=14.0.0" declaration conflicts with npm test and the CI matrix which runs on 18.x, 20.x, and 22.x. Bump to ">=18.0.0" (or ">=20.0.0" for the stable version).

Proposed change
   "engines": {
-    "node": ">=14.0.0"
+    "node": ">=18.0.0"
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 9, The package.json engines.node declaration conflicts
with the test runner (node --test requires Node 18+), so update the
"engines.node" field in package.json (look for the engines.node key) to a
minimum of ">=18.0.0" (or ">=20.0.0" if you want to require the stable runner)
so the local npm test, CI matrix (18.x/20.x/22.x) and engine declaration are
consistent.

… 'test'

On Node 22, 'node --test test/' resolves to loading module 'test' (path
/workspace/test) instead of scanning the directory, which fails CI.
Shell-expand the glob so all Node versions run the same test files.

Co-authored-by: Matheus Marinho <m9tzin@users.noreply.github.com>
@m9tzin
m9tzin merged commit 0ed6c2a into main Apr 26, 2026
7 checks passed
@m9tzin
m9tzin deleted the cursor/faceit-rules-fixes-33a9 branch April 26, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants