{"record":{"id":"cff041f6b001285d","repo":"jackwener/OpenCLI","slug":"trip-com-deals-hub-rendered-but-no-promotion-tiles","errorCode":null,"errorMessage":"Trip.com deals hub rendered but no promotion tiles parsed (the tile markup may have changed)","messagePattern":"Trip\\.com deals hub rendered but no promotion tiles parsed \\(the tile markup may have changed\\)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/trip/deals.js","lineNumber":51,"sourceCode":"    func: async (page, kwargs) => {\n        const limit = parseListLimit(kwargs.limit);\n\n        await page.goto(buildDealsUrl());\n        const waitResult = await page.evaluate(WAIT_FOR_DEALS_JS);\n        if (waitResult === 'captcha') {\n            throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');\n        }\n        if (waitResult !== 'content') {\n            throw new CommandExecutionError(`Trip.com deals page did not render deal tiles (state=${String(waitResult)})`);\n        }\n        const raw = await page.evaluate(buildDealsExtractJs());\n        if (!Array.isArray(raw)) {\n            throw new CommandExecutionError('Trip.com deals DOM extraction returned malformed rows');\n        }\n        // The Top Deals hub is a permanent curated page, so once the wait confirms it\n        // rendered, zero parsed tiles means the tile markup drifted, not an empty hub.\n        if (raw.length === 0) {\n            throw new CommandExecutionError('Trip.com deals hub rendered but no promotion tiles parsed (the tile markup may have changed)');\n        }\n        return raw.slice(0, limit).map((r, i) => ({\n            rank: i + 1,\n            title: r.title,\n            offer: r.offer,\n            discount: r.discount,\n            url: r.url,\n        }));\n    },\n});\n","sourceCodeStart":33,"sourceCodeEnd":62,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/trip/deals.js#L33-L62","documentation":"CommandExecutionError thrown when the Top Deals hub rendered successfully but the extractor parsed zero promotion tiles. Because the hub is a permanent curated page that is never empty, zero rows means the tile markup drifted — the CSS selectors in buildDealsExtractJs no longer match Trip.com's current promotion-tile structure.","triggerScenarios":"Calling the Trip.com deals command when WAIT_FOR_DEALS_JS reports 'content' but page.evaluate(buildDealsExtractJs()) filters every tile out and returns an empty array, e.g. after Trip.com changed tile class names or DOM structure.","commonSituations":"Trip.com redesign of the deals hub; A/B-tested tile markup in your region; renamed data attributes used by the extractor; regional page variants that render tiles differently.","solutions":["Inspect the live Top Deals hub DOM and update the tile selectors in buildDealsExtractJs","Retry later or from a different region to see if an A/B variant is the cause","Update to the latest CLI version which may already track the new markup","Capture and compare page HTML against the selectors the extractor expects"],"exampleFix":"// before\nif (raw.length === 0) {\n    throw new CommandExecutionError('Trip.com deals hub rendered but no promotion tiles parsed (the tile markup may have changed)');\n}\n// after (diagnose markup drift with a probe)\nif (raw.length === 0) {\n    const probe = await page.evaluate(() => document.querySelectorAll('.deal-tile, [data-deal-id]').length);\n    throw new CommandExecutionError(\n        `Trip.com deals hub rendered but no promotion tiles parsed (candidate tiles=${probe}; markup may have changed)`);\n}","handlingStrategy":"validation","validationCode":"// Sanity-check tile markup before trusting zero parses as markup drift\nconst candidateTiles = await page.evaluate(() =>\n    document.querySelectorAll('[class*=deal], [data-deal-id], .promotion').length);\nif (candidateTiles === 0) console.warn('No tile-like nodes at all — page may not be the deals hub');","typeGuard":"function hasParsedTiles(v) {\n    return Array.isArray(v) && v.length > 0 && v.every(t => t && typeof t.title === 'string');\n}","tryCatchPattern":"try {\n    const deals = await runTripDeals();\n} catch (e) {\n    if (e instanceof CommandExecutionError && /no promotion tiles parsed/.test(e.message)) {\n        // markup drift: capture page HTML snapshot and alert maintainers, don't blind-retry\n    } else { throw e; }\n}","preventionTips":["Add a markup-drift CI check comparing extractor selectors against saved hub HTML","Alert with a DOM probe (candidate tile count) instead of silent failures","Track Trip.com redesigns; update selectors promptly"],"tags":["scraping","markup-drift","dom-parsing","trip-com"],"backgroundTag":"selector-markup-drift","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}