Skip to content

Proof of concept: persist Brand Lookup runs server-side - #265

Open
harigenhealth wants to merge 1 commit into
every-app:mainfrom
harigenhealth:feat/persist-brand-lookup-runs
Open

Proof of concept: persist Brand Lookup runs server-side#265
harigenhealth wants to merge 1 commit into
every-app:mainfrom
harigenhealth:feat/persist-brand-lookup-runs

Conversation

@harigenhealth

Copy link
Copy Markdown

Opening this as a proof of concept for an issue, not a merge request — I read docs/CONTRIBUTING.md and know external PRs aren't being merged right now. Feel free to close it and take the idea, or ignore it entirely.

Being upfront on the thing you called out in CONTRIBUTING: this was written with Claude Code. It is tested rather than just generated — details below — but you should weigh it accordingly.

The problem

Brand Lookup fans out to several paid DataForSEO LLM-mentions endpoints, but nothing keeps what a run returned. Results cache in R2 for 24h and the "Recent searches" list is localStorage. Two consequences:

  • History is per-browser. I ran a lookup in one browser and it was invisible in another. On a fresh profile the page looks like nothing ever ran.
  • Nothing retains the numbers. Once the cache expires, re-opening a past lookup re-charges DataForSEO and can return different numbers.

That second one is what pushed me to build something. I'm tracking whether a company shows up in AI answers over time, so I need to compare this month's run to last month's. Right now there's nothing to compare against — the data I paid for is gone in a day.

What this does

Adds a brand_lookup_runs table storing the query, resolved target, scope, competitor set, headline metrics as typed columns, and the full result as a JSON payload. Every successful lookup saves. A "Saved lookups" panel lists them and re-opens one from storage, which costs nothing.

Shape decisions, in case any are useful:

  • Headline metrics are lifted into columns so a trend reads without parsing every payload; the payload keeps everything for re-render.
  • Runs are capped at 100 per project and pruned on write, since payloads run ~80KB.
  • A failed save logs and is swallowed. It must not fail a lookup the user just paid for.
  • Stored payloads are re-validated with brandLookupResultSchema on read, so one written by an older result shape isn't trusted blindly.
  • The Postgres schema is hand-written to match, per the parity test contract.
  • localStorage history is kept as a fast per-browser jump list rather than replaced.

Testing

  • Full suite green: 1118 tests / 133 files, including the 182 schema-parity tests guarding the Postgres twin.
  • tsc --noEmit and oxlint --type-aware both clean.
  • Verified end to end against a real DataForSEO account: ran a live lookup, confirmed the row persisted with correct metrics, then re-opened the saved run and confirmed the account balance did not move.
  • Both migrations generated via db:generate and applied locally.

Happy to write this up as a plain issue instead if that's more useful.

🤖 Generated with Claude Code

Brand Lookup fans out to several paid DataForSEO LLM-mentions endpoints, but
its only memory was a 24h R2 result cache plus a localStorage list of queries.
That made the two things the feature is actually for impossible:

- History was per-browser. A lookup run in one browser was invisible in
  another, and on a fresh profile the page looked like nothing had ever run.
- Nothing kept what a run RETURNED. Once the cache expired, re-opening a past
  lookup re-charged DataForSEO and could come back with different numbers, so
  month-over-month comparison had nothing to compare against.

Adds a brand_lookup_runs table storing the query, resolved target, scope,
competitor set, the headline metrics as typed columns, and the full result as
a JSON payload. Every successful lookup is saved; a Saved lookups panel lists
them and re-opens one from storage, which costs nothing.

Notes on the shape:
- Headline metrics are lifted into columns so a trend reads without parsing
  every payload, while the payload keeps everything for re-render.
- Runs are capped at 100 per project and pruned on write, since payloads are
  large enough that an unbounded table would grow unnoticed.
- A failed save logs and is swallowed: it must not fail the lookup the user
  just paid for.
- Stored payloads are re-validated with brandLookupResultSchema on read, so a
  payload written by an older result shape is not trusted blindly.
- The Postgres schema is hand-written to match, per the parity test contract.

The localStorage history is kept as a fast per-browser jump list; this adds
durable storage alongside it rather than replacing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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