Skip to content

fix: add pagination cursor and totalCount to cyber threats response#237

Closed
haosenwang1018 wants to merge 3 commits into
koala73:mainfrom
haosenwang1018:fix/cyber-pagination-cursor
Closed

fix: add pagination cursor and totalCount to cyber threats response#237
haosenwang1018 wants to merge 3 commits into
koala73:mainfrom
haosenwang1018:fix/cyber-pagination-cursor

Conversation

@haosenwang1018

Copy link
Copy Markdown
Contributor

Problem

listCyberThreats returns pagination: undefined. Clients cannot implement 'next page' semantics — there's no cursor or total count to know if more results exist.

Fix

  • Parse req.pagination.cursor as an offset into the sorted/filtered results
  • Return totalCount (total filtered results before paging)
  • Return nextCursor (offset for next page, empty string when exhausted)
  • Remove the premature slice(0, pageSize) before filtering — now applied after all filters

Ref: PR #106 re-review (NEW-7)
Fixes #202

The previous 0.5° rounding (~50km radius) merged distinct events in
dense urban areas (e.g. Manhattan vs Brooklyn protests on the same
day). Reducing to 0.1° (~10km) preserves neighborhood-level
granularity while still deduplicating true duplicates.

Fixes koala73#204

Signed-off-by: haosenwang1018 <[email protected]>
Replace Math.random() with a djb2 hash seeded by the threat ID,
so the same threat always appears at the same coordinates. This
prevents 'jumping' markers on the map when the same data is
re-fetched.

Fixes koala73#203

Signed-off-by: haosenwang1018 <[email protected]>
The handler previously returned pagination: undefined, making it
impossible for clients to paginate. Now parses cursor as an offset,
returns totalCount of filtered results, and nextCursor for the
next page (empty string when no more results).

Fixes koala73#202

Signed-off-by: haosenwang1018 <[email protected]>
@vercel

vercel Bot commented Feb 23, 2026

Copy link
Copy Markdown

@haosenwang1018 is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@koala73

koala73 commented Feb 23, 2026

Copy link
Copy Markdown
Owner

Thanks again @haosenwang1018 much appreciated! Pls see below, the core pagination is on point - but it introduces a few other issues that you should handle

PR Review: fix: add pagination cursor and totalCount to cyber threats response

PR #237 by haosenwang1018 | 3 commits | +30/-11 across 3 files

Summary

The PR addresses the listCyberThreats endpoint returning undefined pagination data. It:

  1. Adds offset-based pagination (parsing cursor as offset, returning totalCount + nextCursor)
  2. Replaces Math.random() jitter with a deterministic hashJitter function for country centroid fallback
  3. Increases dedup grid precision in unrest module from 0.5 degrees to 0.1 degrees

Issues Found

1. Removing Redis caching is a significant performance regression (High)

The PR removes the entire Redis caching layer (list-cyber-threats.ts lines 8-9, 31-32, 42-44, 114-116 on main). The old code cached responses for 15 minutes, shielding upstream APIs from repeated hits. Now every client request triggers 5 upstream API fetches + GeoIP hydration (up to 250 concurrent IP lookups).

The caching was likely removed because the old cache key didn't account for pagination offsets. The correct fix is to cache the full sorted/filtered result set (keyed by filters only, pre-pagination) and apply offset/pageSize slicing after the cache lookup:

const cacheKey = `${REDIS_CACHE_KEY}:${req.timeRange?.start || 0}:${req.type || ''}:...`;
const cached = await getCachedJson(cacheKey) as CyberThreat[] | null;
if (cached?.length) {
  const paged = cached.slice(offset, offset + pageSize);
  return { threats: paged, pagination: { totalCount: cached.length, nextCursor: ... } };
}
// ... fetch, process, cache full result set, then paginate ...

2. Negative cursor offset not guarded (Medium) — list-cyber-threats.ts:36

const offset = req.pagination?.cursor ? parseInt(req.pagination.cursor, 10) || 0 : 0;

parseInt("-5", 10) || 0 evaluates to -5, and results.slice(-5, ...) reads from the end of the array in JavaScript — producing unexpected results.

Fix: Math.max(0, req.pagination?.cursor ? parseInt(req.pagination.cursor, 10) || 0 : 0)

3. Pagination instability across requests (Low / Design)

Since the entire dataset is re-fetched from upstream on every request (especially now without caching), the data between page 1 and page 2 calls can differ entirely. The offset-based cursor assumes a stable result set. Items may be skipped or duplicated across pages. This is inherent to offset pagination over live data — acceptable for a threat feed, but worth documenting for API consumers.

4. hashJitter JSDoc is inaccurate (Nit) — _shared.ts:196

The comment says "returns a float in [-0.5, 0.5)" but the * 2 on the return line produces [-1.0, 1.0]. The code behavior is correct (matches the old (Math.random() - 0.5) * 2); only the comment is wrong.

5. Unrelated dedup change bundled in PR (Nit) — unrest/v1/_shared.ts:76-77

The change from Math.round(lat * 2) / 2 to Math.round(lat * 10) / 10 changes the dedup grid from ~55km to ~11km — a 5x increase in precision that will produce significantly more unique events. This is a meaningful behavioral change unrelated to cyber pagination and should be in its own PR.

What looks good

  • The pagination logic itself (offset extraction, totalCount, nextCursor calculation, slicing after filtering/sorting) is correct
  • The deterministic hashJitter function is a solid replacement for Math.random() — same threat always maps to the same coordinates now
  • Passing threat.id as seed to getCountryCentroid is the right approach for per-threat determinism
  • Error responses now return a proper pagination structure instead of undefined

@haosenwang1018

Copy link
Copy Markdown
Contributor Author

CI failures. Closing per policy.

koala73 added a commit that referenced this pull request Apr 21, 2026
Fixes 5 findings raised by the multi-agent review of PR #3247:

- #234 P2: Drop dead `deps.log` shim from `deduplicateStories` — caller
  now owns the log line so the param rotted (no test used it post-#3247).
  Removed from JSDoc + signature logic.

- #236 P3: Add defensive warn when `winningIdx === undefined` during
  sidecar `embeddingByHash` population. Shouldn't fire with the current
  `materializeCluster` contract, but catches a future refactor where a
  synthesised rep would silently skip topic grouping.

- #237 P3: Skip `groupTopicsPostDedup` when `cfg.mode === 'jaccard'` —
  the kill-switch path returns an empty `embeddingByHash`, and running
  the secondary pass on it would log a noisy "missing embedding" warn
  every tick. Gate the call site; passthrough primary order.

- #240a P3: Remove dead `top ?? []` fallback after `!Array.isArray(top)`
  already handled the falsy case. Replaced with an explicit Array check
  for the rare "falsy but also not-array" input (defence-in-depth).

- #240b P3: Delete two redundant test blocks — the `titleHashHex tiebreak`
  2-rep fixture (permutation-invariance at 15-rep scale already covers
  this invariant) and the `caller log-line format (regex splice)` describe
  block (the regex lives in seed-digest-notifications.mjs, not brief-dedup;
  the full-flow envelope-cleanliness test exercises the caller end-to-end).

Deferred to follow-up: #235 (structured logParts vs regex splice), #238
(plumb cfg to avoid double env-read), #239 (repo-wide env .trim pattern).

Tests: 5910 pass (was 5913; -3 redundant tests removed). typecheck,
typecheck:api, biome all clean.
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.

Add pagination cursor to cyber threats response

2 participants