Skip to content

feat: add topics endpoint for brand presence Data Insights table | LLMO-3605#1985

Merged
JayKid merged 9 commits intomainfrom
feat/topics-endpoint-LLMO-3605
Mar 23, 2026
Merged

feat: add topics endpoint for brand presence Data Insights table | LLMO-3605#1985
JayKid merged 9 commits intomainfrom
feat/topics-endpoint-LLMO-3605

Conversation

@JayKid
Copy link
Copy Markdown
Contributor

@JayKid JayKid commented Mar 18, 2026

Summary

Implements two endpoints for the Data Insights table in brand-presence-pg, splitting topic summaries from prompt details for better performance:

1. GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/topics (updated)

  • Returns topic-level summaries with promptCount instead of embedding the full items[] array, significantly reducing initial payload size
  • Queries brand_presence_executions via PostgREST, deduplicates by prompt|region_code within each topic, and aggregates into TopicDetail objects
  • Supports pagination (page, pageSize default 20), sorting (name, visibility, mentions, citations, sentiment, popularity, position), and all existing filters (siteId, model/platform, categoryId, topicIds, region, origin)
  • Default pageSize reduced from 100 to 20 to support infinite scroll pagination in the UI

2. GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/topics/:topicId/prompts (new)

  • Returns PromptDetail items for a single topic, loaded lazily when the user expands a topic row
  • Uses buildPromptDetails() to deduplicate by prompt|region_code and build prompt-level items
  • Supports the same filters and pagination as the topics endpoint
  • Uses WEEKS_QUERY_LIMIT (200K) cap consistent with other endpoints

Design Decisions

  • Decoupled endpoints: Splitting topics from prompts avoids loading all prompt data upfront. The UI fetches topic summaries first (with promptCount for display), then lazily loads prompts on expansion.
  • Raw brand_presence_executions query (not the brand_presence_topics_by_date materialized view): The mat view lacks organization_id, prompt, answer, url columns needed for auth validation and PromptDetail items. Raw table approach is consistent with sentiment-overview.
  • Deduplication by prompt|region_code within each topic: Matches original UI behavior, prevents count inflation when brands=all.
  • answer field excluded from SELECT to keep response size manageable.

Related PRs

Test Plan

  • Unit tests for aggregateTopicData (updated: asserts promptCount instead of items.length)
  • Unit tests for buildPromptDetails (5 tests: correct building, deduplication, error handling, null fields)
  • Unit tests for createTopicPromptsHandler (10 tests: bad request, forbidden, valid data, empty data, topic filtering, pagination, query errors, site validation, region/origin filters, brandId filtering)
  • Route tests updated with new topic-prompts routes
  • 100% code coverage maintained
  • Verified Data Insights table loads real data in the UI with lazy-loaded prompts

@github-actions
Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

JayKid added 4 commits March 19, 2026 15:51
…MO-3605

Implements GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/topics
which powers the Data Insights table with topic-level aggregated data.

- Queries brand_presence_executions, deduplicates by prompt|region per topic
- Returns TopicDetail objects with PromptDetail items for expandable rows
- Supports pagination (page, pageSize), sorting (name, visibility, mentions,
  citations, sentiment, popularity), and all existing filters
- Uses WEEKS_QUERY_LIMIT (200K) consistent with sentiment-overview
- Adds comprehensive unit tests for aggregateTopicData and createTopicsHandler

Made-with: Cursor
… UI expectations

Sentiment was computed on a 0-1 scale (positive=1, neutral=0.5,
negative=0) but the UI's formatSentimentFromScore expects 0-100.
This caused all topics to display as "Negative". Changed to
positive=100, neutral=50, negative=0 and return -1 for N/A.

Popularity was returning raw imputed volume numbers (-10/-20/-30)
but the UI expects categorical labels. Added volumeToCategory()
to map averages back to High/Medium/Low/N/A.

Made-with: Cursor
Covers uncovered branches in aggregateTopicData (null field fallbacks)
and sortTopicDetails (unknown sortBy fallback, numeric sorting with
zero/NaN values, null context.data in pagination params).

Made-with: Cursor
…ies from prompt details | LLMO-3605

Separate the topics response into two endpoints for better performance:

1. /topics now returns topic-level summaries with `promptCount` instead of
   embedding the full `items[]` array, reducing initial payload size
2. New /topics/:topicId/prompts endpoint returns PromptDetail items for a
   single topic, loaded lazily when the user expands a topic row

Also reduces default pageSize from 100 to 20 to support infinite scroll
pagination in the UI.

Made-with: Cursor
@JayKid JayKid force-pushed the feat/topics-endpoint-LLMO-3605 branch from 5088fb3 to 9e254d4 Compare March 19, 2026 14:53
calvarezg and others added 4 commits March 20, 2026 15:27
…edded resource

- Count mentions/citations/sources from ALL execution rows per topic
  (not just the deduplicated latest), matching the original UI behavior
- Use PostgREST embedded resource (brand_presence_sources(url_id)) to
  efficiently join source URLs instead of a separate batch query
- Return sourceCount (unique source URLs) at the topic level for the
  "All Citations" column

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@calvarezg calvarezg left a comment

Choose a reason for hiding this comment

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

Please address this before merging:

  • src/controllers/llmo/llmo-brand-presence.js (createTopicPromptsHandler) — decodeURIComponent(topicId) is called without a try-catch; a malformed percent-encoded topicId (e.g. %GG) throws an uncaught URIError resulting in a 500 instead of a 400 — wrap in try/catch and return badRequest('Invalid topic ID encoding').
  • docs/llmo-brandalf-apis/ — No documentation added for the two new endpoints; all other brand presence endpoints have a corresponding .md doc in this folder — add a topics-api.md covering query params, response shape (topicDetails, promptCount, sourceCount, pagination), and example requests/responses.

…docs

- Wrap decodeURIComponent(topicId) in try-catch to return 400 instead
  of an uncaught URIError 500 for malformed percent-encoding
- Add topics-api.md documenting both topics and topic-prompts endpoints

Made-with: Cursor
@JayKid JayKid merged commit 666b0bd into main Mar 23, 2026
20 checks passed
@JayKid JayKid deleted the feat/topics-endpoint-LLMO-3605 branch March 23, 2026 12:48
solaris007 pushed a commit that referenced this pull request Mar 23, 2026
# [1.365.0](v1.364.0...v1.365.0) (2026-03-23)

### Features

* add topics endpoint for brand presence Data Insights table | LLMO-3605 ([#1985](#1985)) ([666b0bd](666b0bd))
@solaris007
Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.365.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants