feat: configurable default market for location/language fallbacks#72
feat: configurable default market for location/language fallbacks#72bookingseo wants to merge 3 commits into
Conversation
|
What do you think about setting these at a project level and storing your choice in the database instead? We could also have env vars, but I think that's more confusing for most people. |
|
Agreed — project-level in the DB is the better home for this. Env vars were the smallest-diff approach, but a per-project default is more discoverable, works for orgs with projects in different markets, and doesn't require touching server config at all. Happy to drop the env vars entirely. Concrete shape I'd rework this PR into:
If that shape works for you I'll rework this PR (same branch). Also fine to split schema+MCP from the UI pieces if you'd rather review them separately. |
|
Reworked to per-project as suggested — pushed as a second commit ( Turns out it's even simpler than I proposed:
Full suite green (583 tests, +6 new for the service validation and market resolution); |
|
Pushed 2f8bf23 with two fixes from self-review:
589 tests green, |
Every location/language fallback was hardcoded to the United States (2840/en), which makes self-hosted deployments outside the US pass locationCode/languageCode on every single call. getDefaultMarket() reads OPENSEO_DEFAULT_LOCATION_CODE / OPENSEO_DEFAULT_LANGUAGE_CODE (language auto-derives from the location's native language; invalid values warn and fall back to the US so a typo can't break calls) and feeds the eight MCP tools that default these args. whoami reports the active defaultMarket so agents can discover it. The web UI's initial location honors VITE_DEFAULT_LOCATION_CODE; the per-browser preference still wins. Explicit args always win — the default only changes what omitted means. The Labs-only market tools (get_ranked_keywords, find_serp_competitors) follow the default when the market object is omitted, guarded so an Ads-provider default (e.g. Iceland) keeps them on the US instead of breaking.
Per review: the projects table already carries locationCode/languageCode
('set during onboarding and reused by every project-scoped data call'),
so the default market belongs there — no schema change, no env vars.
- withMcpProjectAuth exposes the project row it already fetches for the
auth gate; the eight defaulting tools fall back to
project.locationCode/languageCode (zero extra queries).
- list_projects returns each project's locationCode/languageCode (the
whoami field from the previous commit is reverted — an org-level tool
has no single project to speak for).
- CreateProjectModal gains a default-market picker (language derives
from the location's native language); ProjectSettings edits both, with
the language list constrained to what DataForSEO serves for the
location. The keyword picker seeds from the project default; the
per-browser preference still wins once set.
- createProject/updateProject accept optional locationCode/languageCode;
a language-only update is validated against the stored location so an
unserved pair is rejected free instead of failing as a charged
provider error.
- The Labs-only market tools follow the project default only when Labs
serves it (an Ads-data market like Iceland falls back to the US).
…n on language-only updates A tool call overriding only the location was paired with the project's stored language — a Vietnam project querying Germany sent languageCode 'vi' to a paid endpoint. resolveRequestMarket snaps the language to the overridden location's default instead, shared across all six tools. updateProject no longer folds a pre-read location back into the write: a language-only change updates only the language column, so concurrent partial market updates can't clobber each other's half; a location change resolves entirely from its input and skips the read.
2f8bf23 to
1d6203e
Compare
Why
Every location/language fallback is hardcoded to the United States (
2840/en). A self-hosted deployment serving another market has to passlocationCode/languageCodeon every single MCP call and re-pick the location in the UI on every browser. We run OpenSEO for the Vietnamese market and hit this constantly — but the change is generic and benefits any non-US self-hoster.What
Two optional env vars make the default market configurable; nothing changes unless they are set, and explicit args always win — the default only changes what omitted means.
src/server/lib/market-defaults.ts(new):getDefaultMarket()readsOPENSEO_DEFAULT_LOCATION_CODE/OPENSEO_DEFAULT_LANGUAGE_CODE. The language auto-derives from the location's native language when unset (e.g.2704→vi). Invalid values log a warning and fall back to the US — a typo can't take calls down. Memoized; oneopenseo_default_marketlog line at startup.get_serp_results,research_keywords,save_keywords,get_domain_overview,get_domain_keyword_suggestions,get_keyword_metrics,get_local_serp_results,get_google_business_questions. The Labs/language asserts run against the resolved pair, which also fixes a latent edge: an explicitlanguageCodewith an omitted location used to validate against the hardcoded US location.get_ranked_keywords/find_serp_competitors(Labs-only,marketobject): an omittedmarketfollows the default only when it is Labs-served — an Ads-data default like Iceland keeps them on the US instead of producing charged failures. The explicit selector still only accepts the US; extending it per-country is left out of scope here.whoamireports the activedefaultMarket {locationCode, languageCode, label}(additive, optional field) so MCP clients can discover the deployment's default.VITE_DEFAULT_LOCATION_CODE(inlined at build — works withDockerfile.selfhost's runtime build via the new compose passthrough). The existing per-browser localStorage preference still wins.compose.yamlpasses the three vars through;.env.exampledocuments them.Testing
market-defaults.test.ts(env parsing, native-language derivation, unserved-pair fallback, unsupported-code fallback, empty-string handling, memoization) anddataforseo-research-tools.market.test.ts(explicit US wins under a non-US default; omitted market follows a Labs default; Ads-provider default falls back to the US).pnpm ci:checkclean.OPENSEO_DEFAULT_LOCATION_CODE=2704:whoamireports Vietnam, andget_serp_resultswithout alocationCodereturns Vietnamese-market SERPs.Notes
DEFAULT_LANGUAGE_CODEinsrc/server/mcp/schemas.tslost its last consumer and is removed;DEFAULT_LOCATION_CODEstays (US fallback anchor).Greptile Summary
This PR adds configurable project default markets for keyword and SERP workflows. The main changes are:
locationCodeandlanguageCodedefaults.list_projectsnow returns market fields so MCP clients can discover fallback behavior.Confidence Score: 5/5
Safe to merge with low risk.
The changes follow the existing serverFunction, service, and repository boundaries. Market inputs are validated with zod and resolved before provider calls. Targeted tests cover market derivation, validation, and MCP fallback behavior. No blocking correctness or security issues were identified.
No files require special attention.
What T-Rex did
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Client as Web UI / MCP Client participant Auth as withMcpProjectAuth participant Project as ProjectService/Repository participant Resolve as resolveRequestMarket / resolveMarket participant Tool as MCP Tool Handler participant DFS as DataForSEO/Domain/Keyword Service Client->>Auth: call tool with projectId and optional market args Auth->>Project: authorize project and load project defaults Project-->>Auth: "project {locationCode, languageCode}" Auth->>Tool: handler args + project context Tool->>Resolve: combine explicit args with project defaults Resolve-->>Tool: resolved location/language pair Tool->>DFS: provider request using resolved market DFS-->>Tool: market-specific results Tool-->>Client: MCP response + project meta%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Client as Web UI / MCP Client participant Auth as withMcpProjectAuth participant Project as ProjectService/Repository participant Resolve as resolveRequestMarket / resolveMarket participant Tool as MCP Tool Handler participant DFS as DataForSEO/Domain/Keyword Service Client->>Auth: call tool with projectId and optional market args Auth->>Project: authorize project and load project defaults Project-->>Auth: "project {locationCode, languageCode}" Auth->>Tool: handler args + project context Tool->>Resolve: combine explicit args with project defaults Resolve-->>Tool: resolved location/language pair Tool->>DFS: provider request using resolved market DFS-->>Tool: market-specific results Tool-->>Client: MCP response + project metaReviews (1): Last reviewed commit: "fix: resolve market overrides as a pair,..." | Re-trigger Greptile