feat: add sentiment-overview endpoint for brand presence | LLMO-3605#1974
Merged
feat: add sentiment-overview endpoint for brand presence | LLMO-3605#1974
Conversation
Add GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/sentiment-overview
that returns per-week sentiment percentages (positive, neutral, negative) for
the stacked bar chart in the brand-presence-pg dashboard.
Implementation details:
- New handler `createSentimentOverviewHandler` queries `brand_presence_executions`
filtered by org, date range, model, and optional dimensions (site, category,
topic, region, origin).
- JS-level aggregation (`aggregateSentimentByWeek`) groups rows by ISO week and
converts raw counts to percentages with capitalized names and hex color codes,
matching the format the UI chart component expects.
- Uses WEEKS_QUERY_LIMIT (200K) instead of the default QUERY_LIMIT (5K) to avoid
truncating the dataset. A DB-level RPC function is the long-term solution but
this approach unblocks the UI work now.
- Also maps `platform` query param to `model` in `parseFilterDimensionsParams` and
`parseWeeksParams` so the UI's platform filter works correctly with PostgREST.
Made-with: Cursor
… counts The sentiment-overview endpoint was counting every raw row in brand_presence_executions, but the same prompt appears once per brand (e.g., Adobe, Photoshop, Lightroom). With brands=all this inflated totalPrompts and promptsWithSentiment by ~5-6x vs the original page. Fix: deduplicate by (prompt, region_code, topics) within each week, matching the original UI's buildPromptKey logic. Also add region_code and topics to the PostgREST select to support the dedup key. Made-with: Cursor
|
This PR will trigger a minor release when merged. |
- Register sentiment-overview routes in required-capabilities.js (brand:read) to fix route coverage CI check - Add unit tests for toISOWeek, buildPromptKey, aggregateSentimentByWeek, and createSentimentOverviewHandler to meet 100% coverage threshold Made-with: Cursor
Add tests for: - zero-sentiment percentage path (total=0 ternary) - brandId filter when not 'all' - categoryId filter when value is a valid UUID Made-with: Cursor
igor-grubic
approved these changes
Mar 18, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/sentiment-overviewendpoint that returns per-week sentiment distribution (positive / neutral / negative as percentages) for the brand-presence-pg dashboard's stacked bar chart.What's included:
createSentimentOverviewHandlerinllmo-brand-presence.js— queriesbrand_presence_executionsvia PostgREST, filtered by org, date range, model, and optional dimensions (site, category, topic, region, origin)aggregateSentimentByWeek) — groups rows by ISO week, converts raw counts to percentages with capitalized sentiment names and hex color codes matching the UI chart format/brands/all/and/brands/:brandId/variantsgetSentimentOverviewexported fromllmo-mysticat-controller.jsparseFilterDimensionsParamsandparseWeeksParamsnow acceptplatformas an alias formodel, fixing a bug where the UI's platform filter was silently ignoredTrade-offs & Design Decisions
JS aggregation vs DB-level RPC: Ideally, the
GROUP BY week, sentimentaggregation would happen in a PostgreSQL RPC function to avoid transferring raw rows. We chose JS-level aggregation for now to avoid a migration PR inmysticat-data-serviceand unblock the UI work. The row limit is set toWEEKS_QUERY_LIMIT(200K) which covers typical query ranges.WEEKS_QUERY_LIMIT instead of QUERY_LIMIT: The default
QUERY_LIMIT(5,000) was truncating the dataset, causing missing weeks and incorrect totals in the sentiment chart. Using the higher 200K limit (already used by the weeks endpoint) ensures all rows are fetched for accurate aggregation. A DB-level RPC function is the long-term fix.Response format: The response matches the format the original brand-presence UI expects — sentiment values are percentages summing to 100, names are capitalized ("Positive", "Neutral", "Negative"), and each entry includes a hex color string. This allows the UI chart component to consume the data without transformation.
Companion PR
brand-presence-pgdashboardTest Plan
GET /org/:spaceCatId/brands/all/brand-presence/sentiment-overview?startDate=...&endDate=...&platform=chatgpt&siteId=...returns correct weekly sentiment percentagestest/routes/index.test.js)Made with Cursor