{"record":{"id":"50e5cb7c17744821","repo":"koala73/worldmonitor","slug":"exa-api-key-is-required-for-exa-search-adapter","errorCode":null,"errorMessage":"EXA_API_KEY is required for exa-search adapter","messagePattern":"EXA_API_KEY is required for exa-search adapter","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consumer-prices-core/src/adapters/exa-search.ts","lineNumber":136,"sourceCode":"    }\n\n    return targets;\n  }\n\n  private buildQuery(canonicalName: string, currency: string, marketCode: string, template?: string): string {\n    const market = MARKET_NAMES[marketCode] ?? '';\n    if (template) {\n      return template\n        .replace('{canonicalName}', canonicalName)\n        .replace('{currency}', currency)\n        .replace('{market}', market)\n        .trim();\n    }\n    return `${canonicalName} ${market} ${currency} price`.trim();\n  }\n\n  async fetchTarget(ctx: AdapterContext, target: Target): Promise<FetchResult> {\n    if (!this.apiKey) throw new Error('EXA_API_KEY is required for exa-search adapter');\n\n    const { canonicalName, domain, currency, basketSlug } = target.metadata as {\n      canonicalName: string;\n      domain: string;\n      currency: string;\n      basketSlug: string;\n    };\n\n    const searchQuery = this.buildQuery(\n      canonicalName,\n      currency,\n      ctx.config.marketCode,\n      ctx.config.acquisition?.searchQueryTemplate,\n    );\n\n    const body = {\n      query: searchQuery,\n      numResults: 5,","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/adapters/exa-search.ts#L118-L154","documentation":"ExaSearchAdapter.fetchTarget() guards on this.apiKey and throws before any network call. The adapter is constructible with an empty key — the constructor does not validate — so the misconfiguration survives until the first target is fetched, even though the class ships validateConfig(), which reports 'EXA_API_KEY env var is required for adapter: exa-search' for exactly this case.","triggerScenarios":"Constructing ExaSearchAdapter('') or with process.env.EXA_API_KEY undefined, then running the adapter pipeline; executing the exa-search adapter in CI without secrets mounted; skipping the validateConfig() step during adapter bootstrap.","commonSituations":"A new environment (local/CI/staging) missing EXA_API_KEY while production has it; secrets mounted after process start; the exa-search adapter registered as the default for search retailers with no startup config check.","solutions":["Set EXA_API_KEY in the environment the adapter process reads","Call adapter.validateConfig(retailerConfig) during bootstrap and refuse to start when it returns errors","Fail adapter construction on a missing key so the failure happens at wiring time, not per-target","Log which retailer configs select the exa-search adapter so the blast radius is obvious"],"exampleFix":"// before — constructible with an empty key, fails per-target later\nconst adapter = new ExaSearchAdapter(process.env.EXA_API_KEY ?? '');\n\n// after — fail at construction; keep validateConfig as the config-level check\nconst key = process.env.EXA_API_KEY;\nif (!key) throw new Error('EXA_API_KEY is not set — cannot wire exa-search adapter');\nconst adapter = new ExaSearchAdapter(key);","handlingStrategy":"validation","validationCode":"const errs = await adapter.validateConfig(retailerConfig);\nif (errs.length) throw new Error(`exa-search config invalid: ${errs.join('; ')}`);","typeGuard":"function exaAdapterReady(adapter: ExaSearchAdapter): boolean {\n  // mirror the internal guard without triggering the per-target throw\n  return Boolean((adapter as unknown as { apiKey?: string }).apiKey);\n}","tryCatchPattern":"try {\n  await adapter.fetchTarget(ctx, target);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('EXA_API_KEY is required')) {\n    abortRun('exa-search misconfigured'); // config error: stop, do not retry — it cannot succeed\n  }\n  throw err;\n}","preventionTips":["Run validateConfig() for every adapter at startup and abort on any returned error string","Gate adapter wiring on env presence: skip registering exa-search when EXA_API_KEY is unset","Distinguish config errors from network errors in the runner: config errors abort, network errors retry"],"tags":["exa","missing-env-var","adapter","configuration"],"backgroundTag":"missing-env-var","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}