Skip to content

feat: Compare Apps view for cross-app keyword ranking#1

Open
onlyilkr wants to merge 1 commit intosemihcihan:mainfrom
onlyilkr:feature/compare-apps
Open

feat: Compare Apps view for cross-app keyword ranking#1
onlyilkr wants to merge 1 commit intosemihcihan:mainfrom
onlyilkr:feature/compare-apps

Conversation

@onlyilkr
Copy link
Copy Markdown

@onlyilkr onlyilkr commented Apr 23, 2026

Summary

Adds a Compare Apps view to the ASO dashboard so users can evaluate how multiple apps stack up against each other for a chosen set of keywords, side by side.

Context and discussion: #2

Problem

Today the dashboard only surfaces keyword popularity/difficulty/rank for a single selected app at a time. There is no way to ask questions like "Where do my three competitors and I stand on this keyword set?" or "Which keywords am I leading on vs. trailing?" without manually switching apps and mentally diffing columns.

Solution

A new compare mode, reachable from a Compare button next to the sidebar Apps heading. It presents a matrix (keywords × apps) with ranks, rank change deltas, rank-tier color bands, and clear distinctions between not tracked, outside top 200, and not yet researched.

Ranks are derived from the existing aso_keywords.ordered_app_ids top-200 list, which means:

  • No schema changes.
  • An app's rank is available for any keyword it appears in the global top 200, even if the app itself has not added that keyword to its tracked list.

Features

  • Multi-app comparison — pick 2 to 10 owned apps from a chip rail + dropdown picker.
  • Custom keyword selection — pick 1 to 100 keywords from the union of keywords the selected apps track, with tracked by all / any scoping and free-text search.
  • Live-updating matrix — changes to apps or keywords re-fetch the matrix (debounced) and re-render in place.
  • Per-column sorting — sort by keyword, popularity, or any app's rank column.
  • Inline chip removal — remove an app or keyword straight from the matrix row/column header.
  • Fullscreen mode — expand the matrix to the full viewport (⛶ icon, ESC to exit).
  • Persistence — last selection (apps, keywords, sort) is stored in localStorage and restored on reopen.
  • Three distinct empty-cell statesnot tracked by app, outside top 200, not yet researched, each with tooltip + aria-label.

Implementation notes

  • Read-only endpoints, consistent with existing route factory pattern:
    • GET /api/aso/compare/keywords — keyword universe with coverage metadata.
    • POST /api/aso/compare/matrix — per-(app, keyword) cell data.
  • SQL uses a single query per endpoint with json_each to look up positions in ordered_app_ids, mirroring the existing paged keyword handler.
  • Frontend is a single CompareView component + use-compare hook (universe fetch, matrix fetch, persistence). App.tsx gets a small button + conditional render — no structural rewrite.
  • Fullscreen uses a React portal to document.body to bypass a containing-block issue caused by an identity transform on the app shell.
  • Styling reuses existing primitives (Button, Input, Badge, Card) and aligns with the main keyword table's typography (10px uppercase headers, 12px body, JetBrains Mono throughout).

Boundaries respected

  • No changes to keywordPipelineService or keywordWriteRepository (compare is read-only).
  • No changes to aso_keywords, app_keywords, or any other table schema.

Test plan

  • npm run typecheck — clean
  • npm test -- --watchman=false — 393 passing (12 new in cli/db/app-compare.test.ts)
  • npm run build — builds CLI, MCP, and dashboard bundles
  • Manual E2E via node cli/dist/cli.js + browser:
    • Open compare from sidebar, default selection pre-populated
    • Add/remove apps, matrix updates live
    • Sort by any app column
    • Fullscreen expand/collapse (button + ESC)
    • Reload — localStorage restores selection
    • Tooltip/aria messages for each empty-cell state

Docs

  • docs/aso-runtime-flows.md — new read-only compare endpoints documented under Trigger Map and Boundary Ownership.
  • README.md — added a one-line feature bullet.

Scope

Explicitly out of scope for v1 (happy to scope these as follow-ups based on feedback):

  • Shareable URL state / named saved comparisons
  • CSV or Markdown export
  • Sparkline / historical trend per cell

Thanks for building this tool — happy to adjust scope, API shape, or integration approach based on your feedback.

Enables users to compare multiple apps side-by-side across a chosen
keyword set inside the ASO dashboard. Ranks are derived from the
existing aso_keywords.ordered_app_ids top-200 lists so no schema
changes are required and apps that do not explicitly track a keyword
still show a rank when they appear in the global ordering.

Highlights:
- Compare entry point added next to the sidebar "Apps" heading
- Live-updating matrix (rows = keywords, columns = apps) with rank,
  change delta, tier tint and per-cell tooltip states for "outside
  top 200", "not yet researched", and "not tracked by this app"
- Custom keyword picker sourced from a union endpoint with coverage
  metadata (N/M apps tracked) and "tracked by all" toggle
- App picker limited to 2-10 apps, keyword picker limited to 1-100
- localStorage persistence of last compare state (apps, keywords,
  sort) restored when reopening the view
- Fullscreen toggle on the matrix (React portal to bypass fixed
  positioning inside the transformed app shell) with ESC to exit
- Read-only endpoints:
  - GET /api/aso/compare/keywords (keyword universe)
  - POST /api/aso/compare/matrix (per-cell rank data)
- Visual styling aligned with existing table/toolbar conventions
  (JetBrains Mono, 10px uppercase headers, 12px body text)

Tests: 12 new unit tests in cli/db/app-compare.test.ts covering
tracked/untracked cells, outside top-200, not-researched, empty
ordered_app_ids, duplicate input deduplication, and country scoping.
Full suite: 393 passing, typecheck clean, build clean.

Docs: docs/aso-runtime-flows.md notes the new read-only endpoints
and the compare-handlers boundary; README adds a feature bullet.
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