Skip to content

Harden the platform against deploy races and request storms#901

Merged
JoaquinBN merged 6 commits into
devfrom
JoaquinBN/poap-outage-triage
Jul 6, 2026
Merged

Harden the platform against deploy races and request storms#901
JoaquinBN merged 6 commits into
devfrom
JoaquinBN/poap-outage-triage

Conversation

@JoaquinBN

@JoaquinBN JoaquinBN commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Startup migrations now run through a new migrate_with_lock command that holds a PostgreSQL advisory lock (with a RUN_MIGRATIONS_ON_STARTUP opt-out), so concurrent App Runner instances can no longer race each other during deploys. Leaderboard rank updates take a per-type advisory lock and iterate leaderboards in sorted order, keeping concurrent recalculations deadlock-free. API page sizes are capped at 50, the mission contribution_type filter rejects non-numeric values, and the frontend fetches catalog endpoints (contribution types, missions, partners) through bounded fetch-all helpers instead of oversized page_size requests. The portal also stops amplifying load: metrics responses share a 30s cache, session refresh / user load / what's-new checks collapse concurrent callers into a single request, refresh pauses in hidden tabs, and literal undefined/null query params are stripped. The submit form additionally recovers expired reCAPTCHA tokens from the widget instead of blocking submission.

Summary by CodeRabbit

  • New Features

    • Several list views now load complete results automatically, so missions, contribution types, and partner data are more reliably shown without missing items.
    • Submission forms now handle reCAPTCHA expiration and errors more smoothly.
  • Bug Fixes

    • Pagination is now capped at a smaller maximum page size, improving consistency across list pages.
    • Invalid mission filters now return a clear error instead of failing unexpectedly.
    • Session and “what’s new” updates are more efficient by avoiding duplicate requests.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@JoaquinBN, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8e181ab8-fa61-487e-9914-57e89a034533

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed25aa and 06b08b8.

📒 Files selected for processing (41)
  • backend/contributions/tests/test_pagination.py
  • backend/contributions/tests/test_security_boundaries.py
  • backend/contributions/tests/test_submission_limits.py
  • backend/contributions/views.py
  • backend/leaderboard/models.py
  • backend/leaderboard/tests/test_recalculation.py
  • backend/partners/tests/test_partners.py
  • backend/startup.sh
  • backend/utils/management/__init__.py
  • backend/utils/management/commands/__init__.py
  • backend/utils/management/commands/migrate_with_lock.py
  • backend/utils/pagination.py
  • backend/utils/tests/test_migrate_with_lock.py
  • frontend/CLAUDE.md
  • frontend/src/components/Missions.svelte
  • frontend/src/components/Pagination.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/components/portal/PartnerLogoBanner.svelte
  • frontend/src/components/portal/submit-contribution/SubmitContribution.svelte
  • frontend/src/lib/api.js
  • frontend/src/lib/auth.js
  • frontend/src/lib/components/ContributionSelection.svelte
  • frontend/src/lib/missionsStore.js
  • frontend/src/lib/userStore.js
  • frontend/src/lib/whatsNewStore.js
  • frontend/src/routes/AllContributions.svelte
  • frontend/src/routes/CommunityPoaps.svelte
  • frontend/src/routes/ContributionTypeDetail.svelte
  • frontend/src/routes/EcosystemPartners.svelte
  • frontend/src/routes/EditSubmission.svelte
  • frontend/src/routes/Metrics.svelte
  • frontend/src/routes/MySubmissions.svelte
  • frontend/src/routes/ProjectPageEditor.svelte
  • frontend/src/routes/StewardDiscordXP.svelte
  • frontend/src/routes/StewardSubmissions.svelte
  • frontend/src/tests/apiClient.test.js
  • frontend/src/tests/authSession.test.js
  • frontend/src/tests/routes.test.js
  • frontend/src/tests/setupTests.js
  • frontend/src/tests/userStore.test.js
  • frontend/src/tests/whatsNewStore.test.js
📝 Walkthrough

Walkthrough

This PR reduces the maximum pagination page size from 100 to 50 across backend and frontend, adds strict integer validation for the mission contribution_type filter, introduces PostgreSQL advisory locking for both database migrations (new migrate_with_lock command) and leaderboard rank recalculation, refactors frontend API client caching/catalog pagination, adds request deduplication to several frontend stores/auth refresh, and reworks reCAPTCHA token handling.

Changes

Pagination limits and mission filter validation

Layer / File(s) Summary
Backend max page size and tests
backend/utils/pagination.py, backend/contributions/tests/*, backend/partners/tests/test_partners.py
max_page_size lowered to 50; tests updated with page_size=50 and adjusted expected counts.
Mission filter validation
backend/contributions/views.py
contribution_type param now validated as integer, raising ValidationError on invalid input.
Frontend API catalog fetching/caching
frontend/src/lib/api.js, frontend/src/tests/apiClient.test.js
Adds param sanitization, shared metrics cache, and fetchSmallCatalogPages powering getAllContributionTypes, getAllMissions, partnersAPI.listAll.
Frontend consumers and pagination controls
frontend/src/components/..., frontend/src/routes/..., frontend/CLAUDE.md, frontend/src/tests/*
Components/routes switch to getAllX/listAll methods and simplified data normalization; page size dropdown options reduced.

Advisory locking for migrations and leaderboard ranks

Layer / File(s) Summary
migrate_with_lock command
backend/utils/management/..., backend/utils/tests/test_migrate_with_lock.py
New management command acquires a PostgreSQL advisory lock before running migrations, with fallback for non-Postgres backends.
Startup wiring
backend/startup.sh
Conditionally runs migrations via migrate_with_lock based on RUN_MIGRATIONS_ON_STARTUP.
Leaderboard rank locking
backend/leaderboard/models.py, backend/leaderboard/tests/test_recalculation.py
Adds pg_advisory_xact_lock-based locking and deterministic sorted iteration for rank updates; test multiplier setup adjusted.

Frontend request dedup and reCAPTCHA

Layer / File(s) Summary
Auth session refresh dedup
frontend/src/lib/auth.js, frontend/src/tests/authSession.test.js
refreshSession() dedupes concurrent calls; periodic refresh pauses when tab is hidden.
Store request dedup
frontend/src/lib/userStore.js, frontend/src/lib/whatsNewStore.js, tests
loadUser() and whatsNewStore.refresh() cache in-flight promises to dedupe concurrent calls.
reCAPTCHA token handling
frontend/src/components/portal/submit-contribution/SubmitContribution.svelte
Adds getRecaptchaToken(), expired/error callbacks, and separates reCAPTCHA error display.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Startup as startup.sh
  participant Command as migrate_with_lock
  participant DB as PostgreSQL

  Startup->>Command: run migrate_with_lock --noinput
  Command->>DB: ensure_connection()
  alt vendor is postgresql
    Command->>DB: SET lock_timeout = '900s'
    Command->>DB: SELECT pg_advisory_lock(namespace, key)
    Command->>DB: SET lock_timeout = 0
    Command->>Command: run migrate
  else non-postgres
    Command->>Command: run migrate without lock
  end
Loading
sequenceDiagram
  participant Component
  participant apiJs as api.js
  participant Backend

  Component->>apiJs: getAllMissions(params)
  apiJs->>Backend: GET /missions/?page=1&page_size=50
  Backend-->>apiJs: results, next
  loop while next is not null
    apiJs->>Backend: GET /missions/?page=N&page_size=50
    Backend-->>apiJs: results, next
  end
  apiJs-->>Component: { data: aggregated results }
Loading

Possibly related PRs

🚥 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 summarizes the main theme of the changeset: locking down deploy races and reducing request storms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch JoaquinBN/poap-outage-triage

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.

JoaquinBN added 4 commits July 6, 2026 17:57
Database migrations and leaderboard rank updates now run under
PostgreSQL advisory locks so concurrent App Runner instances can no
longer race each other during deploys or contribution bursts; rank
updates also iterate leaderboards in sorted order to keep lock
acquisition deadlock-free. Deploys can skip startup migrations via a
RUN_MIGRATIONS_ON_STARTUP switch. API page sizes are capped at 50, and
the mission contribution_type filter rejects non-numeric values instead
of erroring server-side.

The frontend stops amplifying load: catalog-style endpoints (missions,
contribution types, partners) are fetched through a bounded paginated
helper instead of oversized page_size requests, metrics endpoints share
a 30-second cache, session refresh / user load / what's-new refresh
each collapse concurrent callers into one in-flight request, session
refresh pauses in hidden tabs, and literal "undefined"/"null" query
params are stripped before hitting the API.

- backend/utils/management/commands/migrate_with_lock.py: New management command wrapping migrate in a pg_try_advisory_lock poll loop (timeout/poll configurable via flags or env); no-ops to plain migrate on non-Postgres.
- backend/startup.sh: Uses migrate_with_lock and honors RUN_MIGRATIONS_ON_STARTUP (default true).
- backend/deploy-apprunner.sh, backend/deploy-apprunner-dev.sh: Pass RUN_MIGRATIONS_ON_STARTUP through to App Runner env vars.
- backend/github-actions-role-setup.sh: create-role is now idempotent (updates trust policy when the role exists).
- backend/leaderboard/models.py: update_leaderboard_ranks takes a pg_advisory_xact_lock keyed by leaderboard type inside a transaction; callers iterate leaderboard types sorted; recalculate_all_leaderboards ranks all configured leaderboards instead of a hardcoded list.
- backend/utils/pagination.py: max_page_size 100 -> 50.
- backend/contributions/views.py: MissionViewSet validates contribution_type query param, returning 400 on junk like "undefined".
- frontend/src/lib/api.js: Adds sanitizeRequestParams request interceptor, fetchSmallCatalogPages helper with getAllContributionTypes/getAllMissions/partnersAPI.listAll, and a shared 30s metrics request cache for overview + network-activity.
- frontend/src/lib/auth.js: refreshSession single-flight; periodic refresh skips hidden tabs.
- frontend/src/lib/userStore.js, frontend/src/lib/whatsNewStore.js, frontend/src/lib/missionsStore.js: loadUser/refresh single-flight; missions store uses getAllMissions.
- frontend/src/components/*, frontend/src/routes/*: Swapped page_size=100/200/1000 requests for the bounded fetch-all helpers or page_size<=50.
- backend/*/tests/*, frontend/src/tests/*: Updated for the 50 max page size and new helpers; new tests for migrate_with_lock, the param sanitizer, auth session refresh, and whats-new store dedup.
The contribution submit, edit, and selection flows now load contribution
types through the bounded fetch-all helper instead of raw page_size
requests. The submit form recovers the reCAPTCHA token after expiry by
reading it back from the widget rather than failing with a stale one,
resets it on widget expiry/error, and keeps reCAPTCHA prompts out of
the global error banner. Deploy scripts no longer inject a migrations
toggle into App Runner; startup keeps the RUN_MIGRATIONS_ON_STARTUP
switch (default on) around the advisory-locked migrate, and the GitHub
Actions role setup returns to plain role creation.

- frontend/src/components/portal/submit-contribution/SubmitContribution.svelte: getAllContributionTypes; expired/error callbacks reset recaptchaToken; getRecaptchaToken() falls back to grecaptcha.getResponse; reCAPTCHA errors excluded from the banner.
- frontend/src/lib/components/ContributionSelection.svelte, frontend/src/routes/EditSubmission.svelte: getAllContributionTypes instead of paginated getContributionTypes.
- backend/deploy-apprunner.sh, backend/deploy-apprunner-dev.sh: reverted APP_RUNNER_RUN_MIGRATIONS_ON_STARTUP plumbing; startup.sh documents setting RUN_MIGRATIONS_ON_STARTUP=false once deploy-time migrations exist.
- backend/github-actions-role-setup.sh: reverted to plain create-role.
- backend/CLAUDE.md, CHANGELOG.md: dropped the extra doc bullets and changelog entry.
Page-size dropdowns no longer offer 100: the server caps pages at 50,
so picking 100 silently halved the reachable rows because total pages
were computed from the requested size. The submissions, steward
submissions, and Discord XP tables now stop at 50, as does the
Pagination component default.

Dead code flagged in review is removed: the unpaginated
contribution-type, mission, and partner list helpers (all callers use
the bounded fetch-all variants), the unused URLSearchParams and array
branches of the request-param sanitizer, and the one-caller mission
filter helper is inlined. The migration lock command drops its unused
CLI passthrough and tuning knobs and lets Postgres do the waiting via
lock_timeout plus a blocking advisory lock; the rank-update wrapper
loses its duplicate vendor check; unrelated Mee6 fixture churn is
reverted.

## Claude Implementation Notes
- frontend/src/components/Pagination.svelte, MySubmissions.svelte, StewardSubmissions.svelte, StewardDiscordXP.svelte: pageSizeOptions capped at 50 (two paginators per route).
- frontend/src/lib/api.js: deleted contributionsAPI.getContributionTypes, contributionsAPI.getMissions, partnersAPI.list; sanitizeRequestParams reduced to the plain-object path.
- frontend/src/tests/setupTests.js, routes.test.js: removed/updated mocks for the deleted helpers.
- backend/utils/management/commands/migrate_with_lock.py: rewritten to SET lock_timeout '900s' + blocking pg_advisory_lock (session lock auto-releases on disconnect); only flag left is --noinput; test file rewritten to match.
- backend/leaderboard/models.py: update_leaderboard_ranks always locks inside transaction.atomic (helper already no-ops off Postgres).
- backend/contributions/views.py: inlined _get_valid_contribution_type_param into get_queryset.
- backend/startup.sh: boolean case collapsed to a single [ = "false" ] test.
- backend/api/tests.py, community_xp/tests/test_mee6_sync.py, leaderboard/tests/test_stats.py: Mee6SyncRun fixture page_size back to 1000 (MEE6 fetch size, unrelated to DRF max_page_size).
- frontend/CLAUDE.md: partnersAPI doc now lists listAll; PartnerLogoBanner note updated.
@JoaquinBN JoaquinBN force-pushed the JoaquinBN/poap-outage-triage branch from 8ed25aa to b9f0393 Compare July 6, 2026 16:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/contributions/views.py`:
- Around line 3380-3388: The contribution_type validation in the queryset
filtering logic should preserve exception context when re-raising a
ValidationError. Update the exception handling in the code path that parses
contribution_type in the relevant view method so the serializers.ValidationError
is chained from the caught TypeError/ValueError using from err, keeping the
original traceback available for debugging.

In `@backend/utils/management/commands/migrate_with_lock.py`:
- Around line 12-16: The `_signed_int32` helper is duplicated and should be
centralized to avoid drift. Move the shared 32-bit signed conversion logic into
a utility under backend/utils/ (for example a locks helper) and update
migrate_with_lock.py and leaderboard/models.py to import and use that shared
function. If the advisory-lock code is also duplicated, consider extracting the
common lock-acquisition helper there as well so both call sites use the same
implementation.
- Around line 48-59: The `migrate_with_lock` command uses an f-string for
`cursor.execute("SET lock_timeout = ...")`, which static analysis may flag even
though `LOCK_TIMEOUT_SECONDS` is a fixed module constant. Update the
`migrate_with_lock` flow around `connection.ensure_connection()` and the `with
connection.cursor()` block to either cast the timeout to `int()` at use or add a
brief inline comment explaining that the value is hardcoded and not
user-controlled, so the SQL is safe. Keep the `pg_advisory_lock` logic
unchanged.

In `@backend/utils/tests/test_migrate_with_lock.py`:
- Around line 41-44: The test setup in the migrate_with_lock helper uses nested
with blocks that can be combined, so merge the patch.object calls into a single
with statement to satisfy Ruff SIM117. Update the helper that calls
command.handle to keep the same mocked connections and call_command behavior
while reducing the nested context managers.

In `@frontend/src/lib/api.js`:
- Around line 24-51: The cached response in getCachedMetricsRequest is being
reused by reference, so mutations by one caller can leak to others. Update the
caching path in getCachedMetricsRequest to store and return an immutable or
cloned response object instead of the original axios response, and ensure any
nested data used by callers cannot be mutated through the cache. Keep the
promise deduping behavior intact while making the cached value safe to share
across callers.
- Around line 53-83: The hard page cap in fetchSmallCatalogPages can turn normal
catalog growth into a runtime failure once the result set exceeds 1,000 records.
Update fetchSmallCatalogPages to avoid throwing on SMALL_CATALOG_MAX_PAGES in a
way that breaks callers like getAllContributionTypes, getAllMissions, and
listAll; either increase the cap to leave headroom for expected catalog growth
or return the accumulated results and surface a warning/log when the limit is
reached. Keep the existing paging logic and unique symbols intact while making
the overflow behavior graceful.

In `@frontend/src/lib/auth.js`:
- Line 733: The auth refresh logic in the visibility guard only skips work while
hidden, but it never rechecks immediately when the tab becomes visible again.
Update the session refresh flow in auth.js around the state.isAuthenticated /
document.hidden check to listen for visibilitychange and trigger the existing
refresh path as soon as visibility is restored, so the next timer tick is not
the only catch-up mechanism.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 651d3098-ffa3-40ef-8210-70d47ac23e0a

📥 Commits

Reviewing files that changed from the base of the PR and between 843c56e and 8ed25aa.

📒 Files selected for processing (41)
  • backend/contributions/tests/test_pagination.py
  • backend/contributions/tests/test_security_boundaries.py
  • backend/contributions/tests/test_submission_limits.py
  • backend/contributions/views.py
  • backend/leaderboard/models.py
  • backend/leaderboard/tests/test_recalculation.py
  • backend/partners/tests/test_partners.py
  • backend/startup.sh
  • backend/utils/management/__init__.py
  • backend/utils/management/commands/__init__.py
  • backend/utils/management/commands/migrate_with_lock.py
  • backend/utils/pagination.py
  • backend/utils/tests/test_migrate_with_lock.py
  • frontend/CLAUDE.md
  • frontend/src/components/Missions.svelte
  • frontend/src/components/Pagination.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/components/portal/PartnerLogoBanner.svelte
  • frontend/src/components/portal/submit-contribution/SubmitContribution.svelte
  • frontend/src/lib/api.js
  • frontend/src/lib/auth.js
  • frontend/src/lib/components/ContributionSelection.svelte
  • frontend/src/lib/missionsStore.js
  • frontend/src/lib/userStore.js
  • frontend/src/lib/whatsNewStore.js
  • frontend/src/routes/AllContributions.svelte
  • frontend/src/routes/CommunityPoaps.svelte
  • frontend/src/routes/ContributionTypeDetail.svelte
  • frontend/src/routes/EcosystemPartners.svelte
  • frontend/src/routes/EditSubmission.svelte
  • frontend/src/routes/Metrics.svelte
  • frontend/src/routes/MySubmissions.svelte
  • frontend/src/routes/ProjectPageEditor.svelte
  • frontend/src/routes/StewardDiscordXP.svelte
  • frontend/src/routes/StewardSubmissions.svelte
  • frontend/src/tests/apiClient.test.js
  • frontend/src/tests/authSession.test.js
  • frontend/src/tests/routes.test.js
  • frontend/src/tests/setupTests.js
  • frontend/src/tests/userStore.test.js
  • frontend/src/tests/whatsNewStore.test.js

Comment thread backend/contributions/views.py
Comment on lines +12 to +16
def _signed_int32(value):
return value - 2**32 if value >= 2**31 else value


MIGRATION_LOCK_KEY = _signed_int32(zlib.crc32(b'tally:migrations:default'))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate _signed_int32 helper across files.

The identical _signed_int32 conversion helper is redefined in backend/leaderboard/models.py (used for the leaderboard rank advisory lock key). Consider extracting this (and possibly a shared "acquire advisory lock" helper) into backend/utils/ to avoid divergence if one copy is changed later.

♻️ Suggested consolidation
# backend/utils/locks.py
def signed_int32(value: int) -> int:
    return value - 2**32 if value >= 2**31 else value

Then import signed_int32 from both migrate_with_lock.py and leaderboard/models.py.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 12-12: Missing return type annotation for private function _signed_int32

(ANN202)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/management/commands/migrate_with_lock.py` around lines 12 - 16,
The `_signed_int32` helper is duplicated and should be centralized to avoid
drift. Move the shared 32-bit signed conversion logic into a utility under
backend/utils/ (for example a locks helper) and update migrate_with_lock.py and
leaderboard/models.py to import and use that shared function. If the
advisory-lock code is also duplicated, consider extracting the common
lock-acquisition helper there as well so both call sites use the same
implementation.

Comment thread backend/utils/management/commands/migrate_with_lock.py
Comment thread backend/utils/tests/test_migrate_with_lock.py Outdated
Comment thread frontend/src/lib/api.js
Comment on lines +24 to +51
function getCachedMetricsRequest(key, requestFn) {
const cached = metricsRequestCache.get(key);
const now = Date.now();

if (cached?.response && now - cached.timestamp < METRICS_CACHE_TTL_MS) {
return Promise.resolve(cached.response);
}

if (cached?.promise) {
return cached.promise;
}

const promise = requestFn()
.then((response) => {
metricsRequestCache.set(key, {
response,
timestamp: Date.now(),
});
return response;
})
.catch((error) => {
metricsRequestCache.delete(key);
throw error;
});

metricsRequestCache.set(key, { promise, timestamp: now });
return promise;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cached response object is shared by reference across callers.

cached.response is returned as-is to every caller during the TTL window; any downstream mutation of the axios response (or its .data) would silently corrupt the cache for all other callers until it expires.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/lib/api.js` around lines 24 - 51, The cached response in
getCachedMetricsRequest is being reused by reference, so mutations by one caller
can leak to others. Update the caching path in getCachedMetricsRequest to store
and return an immutable or cloned response object instead of the original axios
response, and ensure any nested data used by callers cannot be mutated through
the cache. Keep the promise deduping behavior intact while making the cached
value safe to share across callers.

Comment thread frontend/src/lib/api.js
Comment thread frontend/src/lib/auth.js
JoaquinBN added 2 commits July 6, 2026 18:17
The mission contribution_type validation error is chained from the
original parse error so tracebacks stay debuggable. The bounded catalog
fetch no longer throws when a catalog outgrows its page cap; it logs a
warning and returns what it has, so dropdowns and catalog pages degrade
instead of crashing. Session refresh now also fires the moment a hidden
tab becomes visible again, since the periodic refresh skips hidden
tabs. The migration lock timeout is cast to int at the SQL boundary and
the lock test collapses its nested context managers.

## Claude Implementation Notes
- backend/contributions/views.py: raise ... from err on the contribution_type ValidationError.
- frontend/src/lib/api.js: fetchSmallCatalogPages returns accumulated results with a console.warn instead of throwing at SMALL_CATALOG_MAX_PAGES.
- frontend/src/lib/auth.js: visibilitychange listener triggers refreshSession when the tab becomes visible while authenticated.
- backend/utils/management/commands/migrate_with_lock.py: int() cast on LOCK_TIMEOUT_SECONDS in the SET lock_timeout f-string.
- backend/utils/tests/test_migrate_with_lock.py: merged nested patch.object with-blocks (SIM117).
- Skipped: _signed_int32 dedup (2-line pure function in two unrelated lock domains; a shared module is more surface than the duplication) and metrics cache cloning (all consumers are read-only and DecisionsChart already copies arrays before chart.js mutates them).
…triage

# Conflicts:
#	frontend/src/lib/userStore.js
@JoaquinBN JoaquinBN merged commit 265e503 into dev Jul 6, 2026
2 of 3 checks passed
@JoaquinBN JoaquinBN deleted the JoaquinBN/poap-outage-triage branch July 6, 2026 17:04
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.

1 participant