feat: activity places filter#87
Conversation
Accepts ?places=id1,id2 alongside existing ?areas= and ?area= params. Place IDs union with area-derived element IDs via HashSet so events covered by both a saved area and an explicitly saved place inside it are deduped naturally. When places are provided, event and comment fetches switch from the per-area optimized query to a global query + post-filter by the combined element set; boost filtering already goes through in_filter (renamed from in_area). Motivation: the FE will introduce a /user/activity page combining a signed-in user's saved places and saved areas into one feed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The helper was renamed when places were added to the element filter; the boost comment still referenced the old name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, ?places=foo silently dropped the invalid segment and fell through to an unfiltered global response — surprising behavior when the caller expects the filter to take effect. Switch filter_map(...ok()) to collect::<Result<_,_>>()? and return 400 Invalid Input on any non-integer segment. Mirrors how ?areas= 404s on an unknown area. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The endpoint is unauthenticated and select_created_between scans all events in the window. Without a ceiling on days, ?days=36500 forces a 100-year scan. Require 1 <= days <= 3650 (10y) and return 400 otherwise. 3650 leaves generous headroom for the area-feed UI's 30-day pagination (≈120 load-mores of headroom) while closing the unbounded-range DoS path; this also protects the pre-existing global fetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
An unbounded places list lets a caller build an arbitrarily large HashSet<i64> from query input. 500 covers realistic power-user saved lists (the planned /user/activity page on the FE); callers with more can fall back to saving the containing area. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
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 32 minutes and 5 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdded Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Addresses Copilot review on PR #87: - discussion_r3108284219: collect directly into HashSet<i64> rather than Vec then insert, removing the redundant conversion. - discussion_r3108284246: MAX_PLACES now caps unique IDs, so a request like places=1,1,1,... is no longer rejected on dup-inflated length. No behavior change for well-formed input; HashSet iteration order is non-deterministic but subsequent code only unions into another HashSet and final results are sorted by created_at. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/rest/v4/activity.rs (1)
162-233: Global fetch + post-filter can scale poorly whenplacesis specified.Whenever
placesis non-empty (with or withoutareas), both the event and comment paths callselect_created_between(day_ago, period_end, ...)and then filter in Rust viain_filter. For a busy deployment with many events/comments in the day window, this loads the full unfiltered result set into memory on every request that uses a narrowplacesfilter — exactly the case where a targeted query would return very few rows.Also, the inline comment on Lines 162/215 lists three behaviors ("area-scoped (optimized), global, or global + post-filter") but the
if/elseonly has two branches; the distinction between plain global and "global + post-filter" is implicit in whetherelementsisSome.Consider adding a
select_created_between_for_element_ids(element_ids, from, to, pool)query (per theblocking_querieslayer convention) and routing theplaces-only andareas + placescases through a push-down filter instead of a global scan. Not a blocker for correctness — the current logic is correct and tests validate it — but worth an issue to track before this endpoint sees heavy use.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rest/v4/activity.rs` around lines 162 - 233, The current logic fetches all events/comments via select_created_between when any places are provided and then post-filters with in_filter, which can OOM/scale poorly; add new pushed-down queries (e.g., db::main::element_event::queries::select_created_between_for_element_ids and db::main::element_comment::queries::select_created_between_for_element_ids following the blocking_queries convention) and route the code paths in activity.rs so that when places (or areas+places) is non-empty you call those element_id-scoped queries instead of select_created_between, keeping the existing area-scoped branch that uses select_created_between_for_area and preserving result dedup/collection logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/rest/v4/activity.rs`:
- Around line 116-133: The current code parses the entire comma-separated string
into a HashSet before enforcing MAX_PLACES, allowing huge inputs to allocate
memory; instead, first count tokens in args.places (e.g., let token_count =
comma_separated_places.split(',').filter(|s| !s.trim().is_empty()).count()) and
immediately return RestApiError::invalid_input if token_count > MAX_PLACES to
cap allocation, then proceed to parse and collect into the HashSet<i64> (the
places variable) as before; keep using the same error message via
RestApiError::invalid_input and the same symbols args.places, places, and
MAX_PLACES to locate where to change the logic.
---
Nitpick comments:
In `@src/rest/v4/activity.rs`:
- Around line 162-233: The current logic fetches all events/comments via
select_created_between when any places are provided and then post-filters with
in_filter, which can OOM/scale poorly; add new pushed-down queries (e.g.,
db::main::element_event::queries::select_created_between_for_element_ids and
db::main::element_comment::queries::select_created_between_for_element_ids
following the blocking_queries convention) and route the code paths in
activity.rs so that when places (or areas+places) is non-empty you call those
element_id-scoped queries instead of select_created_between, keeping the
existing area-scoped branch that uses select_created_between_for_area and
preserving result dedup/collection logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
Addresses CodeRabbit review on PR #87 (discussion_r3108325028): enforce MAX_PLACES on the raw comma-separated token count before building the HashSet, so a pathological input can't allocate a large intermediate set before the guard rejects. Trade-off noted: this reverts the "unique IDs" framing from the prior commit. Legitimate clients (the planned /user/activity page joining session.savedPlaces) don't send duplicates, so a raw-token cap is correct in practice and closes the allocation window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/rest/v4/activity.rs (1)
163-181:⚠️ Potential issue | 🟠 MajorScalability concern: global fetch + in-memory filter for
places.When
placesis provided (with or withoutareas), this path callsselect_created_between(day_ago, period_end, ..)which pulls every event in the window across the whole DB, then discards all but the matchingelement_ids in Rust. The same pattern repeats for comments at lines 216–234. For a small set ofplaces(often just 1–10 IDs) over a 10‑yeardayswindow, this is O(total_events) per request and will degrade as the table grows — especially unfortunate given the area path already has an optimized per-area query.Consider adding a
select_created_between_for_elements(ids, from, to, ..)query inblocking_queries/queries(SQLWHERE element_id IN (...)or a bulk fetch) and using it whenplacesis non-empty; union the results with the per-area fetch when both are supplied. This keeps both filter axes on the DB side and preserves the dedupe semantics the PR is after.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rest/v4/activity.rs` around lines 163 - 181, The current code pulls all events via select_created_between when any places are provided, causing O(total_events) work; add a new DB query (e.g., element_event::queries::select_created_between_for_elements(ids, from, to, &pool)) that does a WHERE element_id IN (...) (implement in blocking_queries/queries SQL), then in src/rest/v4/activity.rs call that function when places is non-empty (and still call the per-area select_created_between_for_area for each area when areas is also supplied), merge/union the results (use the existing HashSet<ElementEvent> dedupe pattern) and map DB errors to RestApiError::database() as before; apply the same change for the comments path (the select_created_between usage referenced around lines 216–234) using a corresponding comment query.
🧹 Nitpick comments (1)
src/rest/v4/activity.rs (1)
635-829: Good test coverage for the new surface.Places-only, area+place inside (dedupe), area+place outside (union), oversized places list, out-of-range
days, and invalidplacestokens are all covered. One small gap: there's no test asserting that supplyingplacesstill returns comments and boosts (only events are exercised via places); worth adding if you want to lock in the comment/boostin_filterpaths against regression.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/rest/v4/activity.rs` around lines 635 - 829, Add a test that mirrors get_filtered_by_places but also inserts a comment and a boost tied to one of the places (use db::main::comment::queries::insert and db::main::boost::queries::insert or the equivalent helpers), call the handler via super::get with the places query, and assert the returned Vec<super::ActivityItem> contains items for those comment and boost records (check identifying ActivityItem fields like comment_id/boost_id or kind) to ensure comments and boosts follow the same places filtering/in_filter path as element events.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/rest/v4/activity.rs`:
- Around line 116-134: Parse and deduplicate tokens before enforcing MAX_PLACES:
in the block handling args.places (the computation of places), first split and
map trimming, filter out empty/whitespace tokens (so trailing commas or
duplicated separators are ignored), parse remaining tokens to i64, collect into
a Result<HashSet<_>, _>, then check the HashSet.len() against MAX_PLACES and
return RestApiError::invalid_input with the existing "places must contain at
most {MAX_PLACES} IDs" message if exceeded; keep the existing parse-error
message for non-integer tokens and ensure None still yields an empty HashSet.
---
Outside diff comments:
In `@src/rest/v4/activity.rs`:
- Around line 163-181: The current code pulls all events via
select_created_between when any places are provided, causing O(total_events)
work; add a new DB query (e.g.,
element_event::queries::select_created_between_for_elements(ids, from, to,
&pool)) that does a WHERE element_id IN (...) (implement in
blocking_queries/queries SQL), then in src/rest/v4/activity.rs call that
function when places is non-empty (and still call the per-area
select_created_between_for_area for each area when areas is also supplied),
merge/union the results (use the existing HashSet<ElementEvent> dedupe pattern)
and map DB errors to RestApiError::database() as before; apply the same change
for the comments path (the select_created_between usage referenced around lines
216–234) using a corresponding comment query.
---
Nitpick comments:
In `@src/rest/v4/activity.rs`:
- Around line 635-829: Add a test that mirrors get_filtered_by_places but also
inserts a comment and a boost tied to one of the places (use
db::main::comment::queries::insert and db::main::boost::queries::insert or the
equivalent helpers), call the handler via super::get with the places query, and
assert the returned Vec<super::ActivityItem> contains items for those comment
and boost records (check identifying ActivityItem fields like
comment_id/boost_id or kind) to ensure comments and boosts follow the same
places filtering/in_filter path as element events.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
Addresses CodeRabbit nit on PR #87 (discussion_r3108598972): the cap is enforced on raw comma-separated tokens, but the error message said "IDs" which implies a uniqueness-aware limit. Switch to "comma-separated values" so the message matches what's actually counted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
The activity feed endpoint was shipped without a corresponding doc (same as #86's top-editors). Adds docs/rest/v4/activity.md covering parameters (days / area / areas / places), response shape, examples, and error cases, plus a link in the v4 README under "Implemented". Includes the `?places=` parameter added in #87, so this branch should merge after that one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
The activity feed endpoint was shipped without a corresponding doc (same as #86's top-editors). Adds docs/rest/v4/activity.md covering parameters (days / area / areas / places), response shape, examples, and error cases, plus a link in the v4 README under "Implemented". Includes the `?places=` parameter added in #87, so this branch should merge after that one. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Relates to this FE draft: teambtcmap/btcmap.org#928
What
Adds a
?places=id1,id2,...query param toGET /v4/activity, alongsidethe existing
?area=/?areas=filters. Place IDs union into theelement-filter
HashSetso area+place overlap (e.g. saved country + aplace inside it) dedupes to one event.
Why
The FE is building
/user/activity— a combined activity feed across asigned-in user's saved places and saved areas. One request, server-side
merge + dedup, stateless/cacheable (no
/users/me/activityneeded).Validation & limits
placesvalues must parse asi64→ otherwise400 Invalid Input.daysclamped to1..=3650→ otherwise400(also protects thepre-existing unauthenticated global fetch path).
placescapped at 500 unique IDs.Tests
cargo test activity→ 16 passing, incl. new cases for:?places=...only (pure places filter)?areas=A&places=Xwhere X is inside A → deduped to 1 event?areas=A&places=Xwhere X is outside A → both includedplaces, out-of-rangedays, oversizedplaceslist → 400Notes
No SQL changes, no new queries. The handler falls back from the per-area
optimized fetch to a global fetch + post-filter whenever
placesispresent; existing area-only callers are unchanged.
Summary by CodeRabbit
New Features
Bug Fixes