Skip to content

feat: add sentiment-overview endpoint for brand presence | LLMO-3605#1974

Merged
JayKid merged 5 commits intomainfrom
feat/sentiment-overview-LLMO-3605
Mar 18, 2026
Merged

feat: add sentiment-overview endpoint for brand presence | LLMO-3605#1974
JayKid merged 5 commits intomainfrom
feat/sentiment-overview-LLMO-3605

Conversation

@JayKid
Copy link
Contributor

@JayKid JayKid commented Mar 17, 2026

Summary

Adds a new GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/sentiment-overview endpoint that returns per-week sentiment distribution (positive / neutral / negative as percentages) for the brand-presence-pg dashboard's stacked bar chart.

What's included:

  • New handler createSentimentOverviewHandler in llmo-brand-presence.js — queries brand_presence_executions via PostgREST, filtered by org, date range, model, and optional dimensions (site, category, topic, region, origin)
  • JS-level aggregation (aggregateSentimentByWeek) — groups rows by ISO week, converts raw counts to percentages with capitalized sentiment names and hex color codes matching the UI chart format
  • Route registration — both /brands/all/ and /brands/:brandId/ variants
  • Controller wiringgetSentimentOverview exported from llmo-mysticat-controller.js
  • Platform-to-model mapping fixparseFilterDimensionsParams and parseWeeksParams now accept platform as an alias for model, fixing a bug where the UI's platform filter was silently ignored

Trade-offs & Design Decisions

  1. JS aggregation vs DB-level RPC: Ideally, the GROUP BY week, sentiment aggregation would happen in a PostgreSQL RPC function to avoid transferring raw rows. We chose JS-level aggregation for now to avoid a migration PR in mysticat-data-service and unblock the UI work. The row limit is set to WEEKS_QUERY_LIMIT (200K) which covers typical query ranges.

  2. 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.

  3. 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

Test Plan

  • Verify GET /org/:spaceCatId/brands/all/brand-presence/sentiment-overview?startDate=...&endDate=...&platform=chatgpt&siteId=... returns correct weekly sentiment percentages
  • Verify sentiment percentages sum to 100 for each week
  • Verify all weeks in the date range are represented (no truncation)
  • Verify optional filters (category, topic, region, origin) narrow results correctly
  • Route registration tests pass (test/routes/index.test.js)

Made with Cursor

JayKid added 2 commits March 17, 2026 17:42
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
@github-actions
Copy link

This PR will trigger a minor release when merged.

JayKid added 3 commits March 18, 2026 10:00
- 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
@codecov
Copy link

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 JayKid merged commit 6ec5cbf into main Mar 18, 2026
18 checks passed
@JayKid JayKid deleted the feat/sentiment-overview-LLMO-3605 branch March 18, 2026 13:40
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.

2 participants