{"record":{"id":"8907b752be669996","repo":"moeru-ai/airi","slug":"failed-to-extract-tweet-data","errorCode":null,"errorMessage":"Failed to extract tweet data","messagePattern":"Failed to extract tweet data","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"integrations/twitter-services/src/core/services/tweet.ts","lineNumber":285,"sourceCode":"  /**\n   * Gets detailed information about a specific tweet\n   */\n  async function getTweetDetails(tweetId: string): Promise<TweetDetail> {\n    try {\n      const page = ctx.page\n      await page.goto(`${TWITTER_BASE_URL}/i/status/${tweetId}`)\n      await page.waitForSelector(SELECTORS.TIMELINE.TWEET)\n\n      // Get the main tweet element\n      const tweetElement = await page.$(SELECTORS.TIMELINE.TWEET)\n      if (!tweetElement) {\n        throw new Error('Tweet element not found')\n      }\n\n      // Use the TweetParser to extract the main tweet data\n      const mainTweet = await TweetParser.extractTweetData(page, tweetElement)\n      if (!mainTweet) {\n        throw new Error('Failed to extract tweet data')\n      }\n\n      // Check for quoted tweet\n      let quotedTweet: Tweet | undefined\n      const quotedTweetElement = await page.$('[data-testid=\"quotedTweet\"]')\n      if (quotedTweetElement) {\n        const extractedQuotedTweet = await TweetParser.extractTweetData(page, quotedTweetElement)\n        if (extractedQuotedTweet) {\n          quotedTweet = extractedQuotedTweet\n        }\n      }\n\n      // Get replies by scrolling to load more using reusable scroll logic\n      const replySelector = '[data-testid=\"tweet\"][aria-labelledby*=\"reply\"]'\n\n      // Try to load at least 10 replies (if available)\n      await scrollToLoadMoreTweets(page, 10, replySelector)\n","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/integrations/twitter-services/src/core/services/tweet.ts#L267-L303","documentation":"TweetParser.extractTweetData was given a non-null tweet element but returned a falsy value, meaning it could not pull the required fields (author, text, id, timestamp) out of the DOM. This indicates the tweet element matched but its internal structure differs from what the parser expects.","triggerScenarios":"Twitter changed the internal layout of a tweet (avatar/username/text spans restructured); the matched element is a retweet header or quoted tweet container rather than a full tweet; a required sub-selector returned nothing so the parser bailed.","commonSituations":"Parser written against an older DOM layout; extracted element is a quoted tweet partial; new UI variant (e.g. condensed timeline) served to the bot.","solutions":["Update TweetParser's sub-selectors against the current live tweet DOM.","Ensure the element passed in is a full tweet article, not a quoted/partial fragment.","Have the parser log which required field failed so the failing selector is identifiable.","Pin the automation to a stable Twitter surface (e.g. the /i/status/ page) where the layout is most consistent."],"exampleFix":"// before\nconst mainTweet = await TweetParser.extractTweetData(page, tweetElement)\nif (!mainTweet) {\n  throw new Error('Failed to extract tweet data')\n}\n\n// after\nconst mainTweet = await TweetParser.extractTweetData(page, tweetElement)\nif (!mainTweet) {\n  const html = await page.evaluate(el => el.outerHTML, tweetElement).catch(() => '<unreadable>')\n  throw new Error(`Failed to extract tweet data; element snapshot: ${html.slice(0, 500)}`)\n}","handlingStrategy":"validation","validationCode":"// sanity-check the element is a full tweet article, not a quoted fragment\nconst isArticle = await page.evaluate(\n  el => !!el?.querySelector('[data-testid=\"tweetText\"]') && !!el?.querySelector('[role=\"link\"]'),\n  tweetElement,\n)\nif (!isArticle) throw new Error('Element is not a full tweet article')","typeGuard":"function hasTweetData(t: unknown): t is { id: string; text: string; author: unknown } {\n  return !!t && typeof (t as any).id === 'string' && typeof (t as any).text === 'string'\n}","tryCatchPattern":"const data = await TweetParser.extractTweetData(page, tweetElement)\nif (!data) {\n  // log DOM snapshot for diagnosis, then degrade\n  return null\n}","preventionTips":["Keep TweetParser selectors versioned and tested against saved HTML fixtures.","Pass only full tweet article elements to the parser.","Log which sub-field failed extraction to accelerate fixes."],"tags":["dom-selector","twitter","parser","structure-drift"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}