{"record":{"id":"d60c7fabea71e882","repo":"jackwener/OpenCLI","slug":"booking-com-hotel-row-is-missing-stable-name-url-i","errorCode":null,"errorMessage":"Booking.com hotel row is missing stable name/url identity","messagePattern":"Booking\\.com hotel row is missing stable name/url identity","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/booking/search.js","lineNumber":322,"sourceCode":"        totalText\n          ? `No hotels rendered (${totalText}). Try a broader destination, different dates, or check the URL in a browser.`\n          : 'No hotels rendered. Try a broader destination, different dates, or check the URL in a browser.',\n      );\n    }\n\n    return items.slice(0, limit).map((it, i) => {\n      if (!it || typeof it !== 'object') {\n        throw new CommandExecutionError('Booking.com extractor returned malformed hotel row');\n      }\n      const name = String(it.name || '').trim();\n      const country = String(it.country || '').trim();\n      const slug = String(it.slug || '').trim();\n      const urlValue = String(it.url || '').trim();\n      const expectedUrl = country && slug\n        ? `https://www.booking.com/hotel/${country}/${slug}.html`\n        : '';\n      if (!name || !/^[a-z]{2}$/.test(country) || !slug || urlValue !== expectedUrl) {\n        throw new CommandExecutionError('Booking.com hotel row is missing stable name/url identity');\n      }\n      return {\n        rank: offset + i + 1,\n        name,\n        country,\n        slug,\n        star_rating: it.star_rating,\n        review_score: it.review_score,\n        review_count: it.review_count,\n        price_amount: it.price_amount,\n        price_currency: it.price_amount == null ? '' : (currency || it.price_currency || ''),\n        distance: it.distance,\n        recommended_room: it.recommended_room,\n        url: urlValue,\n      };\n    });\n  },\n});","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/booking/search.js#L304-L340","documentation":"This CommandExecutionError is thrown during post-processing of scraped Booking.com hotel rows when a row lacks the stable identity the CLI guarantees: a non-empty name, a two-letter lowercase country code, a non-empty slug, and a url that exactly equals the canonical https://www.booking.com/hotel/<country>/<slug>.html form. It exists so downstream consumers can rely on every row having a deterministic, reconstructible hotel URL rather than a transient DOM-derived value. It indicates the page extractor produced a row whose DOM shape the extractor did not fully capture.","triggerScenarios":"Running the booking search command when the page extractor returns an item where: it.name is empty, it.country is missing or not a 2-letter lowercase code, it.slug is empty, or it.url does not exactly match the constructed canonical hotel URL. Booking.com markup changes, alternative hotel card layouts, locale variants emitting uppercase/region country codes, or hotels whose canonical URL deviates from the /hotel/<country>/<slug>.html pattern all trigger it.","commonSituations":"A Booking.com A/B test or redesign changes hotel card DOM attributes so the extractor misses name/slug/url fields; scraping a non-standard property type (e.g. hostels/apartments with different URL structure); country code returned as 'GB' or 'en-gb' instead of 'gb'; new marketplace domains (e.g. .co.uk or b-hotel URLs) failing the equality check.","solutions":["Re-run the search with a different locale (--lang en) or destination to see if only certain layouts fail","Update the Booking.com extractor to pick up the new hotel-card DOM selectors so name/slug/url are captured","Check Booking.com HTML for the changed card structure and add the missing selector to the in-page extraction script","If a specific property type never matches the /hotel/<country>/<slug>.html pattern, extend expectedUrl construction or relax the strict equality while keeping the invariant documented"],"exampleFix":"// before (extractor misses url on new card layout)\nurl: card.querySelector('a.hotel-card-link')?.href || ''\n// after\ntarget_url: card.querySelector('a[data-testid=\"property-card-desktop-link\"], a.hotel-card-link')?.getAttribute('href') || ''","handlingStrategy":"validation","validationCode":"// Validate extracted rows before calling/consuming the search result\nfunction hasStableHotelIdentity(it) {\n  if (!it || typeof it !== 'object') return false;\n  const name = String(it.name || '').trim();\n  const country = String(it.country || '').trim();\n  const slug = String(it.slug || '').trim();\n  const url = String(it.url || '').trim();\n  const expected = country && slug ? `https://www.booking.com/hotel/${country}/${slug}.html` : '';\n  return !!name && /^[a-z]{2}$/.test(country) && !!slug && url === expected;\n}\nif (!raw.items.every(hasStableHotelIdentity)) {\n  console.warn('some Booking.com rows lack stable identity; extractor may be stale');\n}","typeGuard":"function isWellFormedHotelRow(it) {\n  return (\n    typeof it === 'object' && it !== null &&\n    typeof it.name === 'string' && it.name.trim() !== '' &&\n    typeof it.slug === 'string' && it.slug.trim() !== '' &&\n    typeof it.country === 'string' && /^[a-z]{2}$/.test(it.country) &&\n    typeof it.url === 'string' &&\n    it.url === `https://www.booking.com/hotel/${it.country}/${it.slug}.html`\n  );\n}","tryCatchPattern":"try {\n  const results = await runBookingSearch(opts);\n} catch (err) {\n  if (err.message.includes('missing stable name/url identity')) {\n    // fall back to raw rows or flag extractor drift\n    console.warn('Booking extractor layout drift; skipping identity check or re-extract');\n  } else throw err;\n}","preventionTips":["Pin/monitor Booking.com DOM changes; add an integration test asserting extracted rows pass the identity invariant","Prefer the canonical data-testid selectors Booking.com uses for property-card links","Normalize locale/country codes to lowercase two-letter form at extraction time","Handle special property types (apartments, hostels) whose URLs may deviate from /hotel/<country>/<slug>.html"],"tags":["scraping","dom-parse","data-integrity","booking"],"backgroundTag":"scraper-dom-structure-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}