feat: brand-vs-competitors API endpoint with aggregate mode#2028
Open
rainer-friederich wants to merge 8 commits intomainfrom
Open
feat: brand-vs-competitors API endpoint with aggregate mode#2028rainer-friederich wants to merge 8 commits intomainfrom
rainer-friederich wants to merge 8 commits intomainfrom
Conversation
|
This PR will trigger a minor release when merged. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Implements the two-step query pattern for the brand_vs_competitors_by_date view from data-service PR-206: first get available execution dates for a site, then query competitor aggregation data for selected dates. This keeps date-range logic in the application layer while the DB provides simple view-based aggregation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gREST calls Replace the two separate endpoints (execution-dates + brand-vs-competitors) with a single combined endpoint that internally performs both PostgREST queries: first discovers execution dates from brand_presence_executions, then queries the brand_vs_competitors_by_date view with those dates. Accepts siteId (required) + optional startDate/endDate/model/categoryName/ regionCode. Defaults to 28-day date range like other brand-presence endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 401/500 responses to OpenAPI spec for brand-vs-competitors endpoint - Add required and additionalProperties: false to BrandVsCompetitorsResponse schema - Add UUID validation for siteId query parameter - Add platform alias for model parameter (consistent with other handlers) - Add missing test for Step 2 view query error path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents silent data truncation when a site has many execution rows per date (multiple brands/models/categories). Same rationale as the weeks handler which also needs all rows to extract distinct values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…regate param Replace the two-step PostgREST pattern (discover dates from brand_presence_executions, then chunked .in() queries against the view) with a single direct query using .gte()/.lte() date-range filters on brand_vs_competitors_by_date. The regular VIEW supports WHERE pushdown into partition-pruned scans, making the separate date discovery step unnecessary. Add aggregate=true query parameter that rolls up across categoryName/regionCode server-side, producing one row per (competitor, executionDate) — the shape the Market Tracking chart needs directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aggregate mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0b7c27b to
d012cdb
Compare
Contributor
Author
Code reviewFound 1 issue:
spacecat-api-service/src/controllers/llmo/llmo-brand-presence.js Lines 2590 to 2592 in d012cdb Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…ema fields
- Replace `params.model || 'chatgpt'` with `resolveModelFromRequest(params.model)`
to match all other handlers ('chatgpt' is not a valid llm_model enum value)
- Use WEEKS_QUERY_LIMIT (200K) instead of QUERY_LIMIT (5K) to avoid silent
truncation — the view can return 60K+ rows for orgs with many competitors,
categories, and regions
- Add required fields to BrandVsCompetitorsResponse item schema so the
frontend knows which fields are guaranteed (categoryName/regionCode remain
optional since they are omitted when aggregate=true)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 IT tests covering: validation (missing/invalid siteId), access control (site not in org, denied user), date range filtering, category filtering, empty results, camelCase response shape, aggregate=true rollup with correct sums, and brandId-scoped queries. docker-compose.yml now accepts IT_DATA_SERVICE_IMAGE env var to override the data-service image — build locally from the view branch with: cd apps/mysticat-data-service git fetch origin feat/brand-vs-competitors-view docker build -f docker/Dockerfile -t mysticat-data-service:local-with-view . IT_DATA_SERVICE_IMAGE=mysticat-data-service:local-with-view npx mocha ... Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
GET /org/:spaceCatId/brands/{all,:brandId}/brand-presence/brand-vs-competitorsendpointbrand_vs_competitors_by_dateVIEW with date-range filters (.gte()/.lte()) — PostgreSQL pushes WHERE clauses through the GROUP BY into partition-pruned, index-covered scanssiteId(required) + optionalstartDate/endDate/model/categoryName/regionCode/aggregate, defaulting to a 28-day date rangeaggregate=truerolls up acrosscategoryName/regionCodeserver-side, producing one row per(competitor, executionDate)— the shape the Market Tracking chart needs directly. Filters still apply before aggregation.resolveModelFromRequestfor model resolution (consistent with all other handlers)WEEKS_QUERY_LIMIT(200K) to avoid silent data truncation for orgs with many competitors/categories/regionsrequiredfields so the frontend knows which properties are guaranteedDepends on
brand_vs_competitors_by_dateregular VIEWResponse shape
Default (
aggregateomitted orfalse): rows at(competitor, executionDate, categoryName, regionCode)granularity — includescategoryNameandregionCodefields.aggregate=true: rows at(competitor, executionDate)granularity — omitscategoryNameandregionCode, sumstotalMentions/totalCitationsacross them.Test plan
npm run docs:lint)Running IT tests locally
The IT tests require the
brand_vs_competitors_by_dateview from data-service PR #206. Build the image locally:Once PR #206 merges and a new ECR image is published, update the default image tag in
test/it/postgres/docker-compose.yml.🤖 Generated with Claude Code