{"record":{"id":"a343c7e5529372e9","repo":"jackwener/OpenCLI","slug":"linkedin-sent-invitations-contained-a-malformed-in","errorCode":null,"errorMessage":"LinkedIn sent invitations contained a malformed invitation card.","messagePattern":"LinkedIn sent invitations contained a malformed invitation card\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/sent-invitations.js","lineNumber":105,"sourceCode":"  browser: true,\n  args: [],\n  columns: ['rank', 'name', 'profile_url', 'invited_date_text'],\n  func: async (page) => {\n    if (!page) throw new CommandExecutionError('Browser session required for linkedin sent-invitations');\n    await page.goto(SENT_URL);\n    await page.wait(12);\n    let result = unwrapEvaluateResult(await page.evaluate(buildSentInvitationsScript()));\n    if (result?.authRequired) {\n      throw new AuthRequiredError(LINKEDIN_DOMAIN, 'LinkedIn sent invitations requires an active signed-in browser session.');\n    }\n    if (result?.warning) {\n      throw new CommandExecutionError('LinkedIn warning/restriction state visible on sent invitations page.');\n    }\n    if (!result || typeof result !== 'object' || Array.isArray(result) || !Array.isArray(result.rows)) {\n      throw new CommandExecutionError('LinkedIn sent invitations returned a malformed extraction payload.');\n    }\n    if (result.malformedCount > 0) {\n      throw new CommandExecutionError('LinkedIn sent invitations contained a malformed invitation card.');\n    }\n    const rows = result.rows;\n    if (rows.length === 0) {\n      if (result.explicitEmpty) {\n        throw new EmptyResultError('linkedin sent-invitations', 'No pending sent invitations were found.');\n      }\n      throw new CommandExecutionError('LinkedIn sent invitation cards were not found; the page structure may have changed.');\n    }\n    return rows.map((row, index) => ({\n      rank: index + 1,\n      name: row.name || '',\n      profile_url: row.profile_url || '',\n      invited_date_text: row.invited_date_text || '',\n    }));\n  },\n});\n\nexport const __test__ = {","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/sent-invitations.js#L87-L123","documentation":"Card candidates that have both a profile link and a Withdraw action but yield no parseable name or profile URL are counted in malformedCount. The command treats any malformed card as a hard failure (no silent partial data for CRM reconciliation) and throws this CommandExecutionError listing nothing but signaling the DOM produced an unparseable invitation card.","triggerScenarios":"Running `opencli linkedin sent-invitations` when at least one invitation card on the page fails parsing: no name derivable from the withdraw aria-label, profile link text, or card lines; or the /in/ link's href is empty — i.e. LinkedIn renders a card variant the extractors do not recognize.","commonSituations":"LinkedIn UI update changing invitation card markup (new layout, renamed aria-labels); a card for a deleted/deactivated member rendering with missing name/href; localized UI text breaking the /^withdraw .../i and 'Sent X days ago' regexes; injected browser extensions altering the DOM.","solutions":["Inspect the invitation-manager page in the browser to see which card fails to parse; if a member was deleted/deactivated, that card may be inherently empty — consider handling it upstream in the extraction script.","Update the extraction selectors/regexes in buildSentInvitationsScript to match the current LinkedIn card markup and aria-label patterns.","Disable browser extensions in the automation profile and retry with the UI language set to English so the /^withdraw/i and date regexes match.","Clear the checkpoint by reloading the page; if only transient rendering caused it, a re-run after a full load (increase the 12s wait) may parse all cards."],"exampleFix":"// before (strict fail on any malformed card)\nif (result.malformedCount > 0) throw new CommandExecutionError('...malformed invitation card.');\n// after (tolerate known-empty cards for deleted members)\nif (result.malformedCount > 0) {\n  console.error(`[linkedin] skipped ${result.malformedCount} unparseable card(s)`);\n  // or: filter candidate cards lacking a[href*=\"/in/\"] before counting\n}","handlingStrategy":"fallback","validationCode":"null","typeGuard":"const isWellFormedCard = (card) => {\n  const link = card?.querySelector?.('a[href*=\"/in/\"]');\n  const name = link?.innerText || link?.textContent || link?.getAttribute('aria-label') || '';\n  return Boolean(name.trim()) && Boolean(link?.getAttribute('href'));\n};","tryCatchPattern":"try {\n  const rows = await run(['linkedin', 'sent-invitations']);\n} catch (e) {\n  if (/malformed invitation card/.test(e.message)) {\n    console.warn('LinkedIn card markup not fully parseable; falling back to manual export of the invitation manager page.');\n    // Fall back to a manual/CSV flow instead of failing the CRM sync\n    return manualInvitationExport();\n  }\n  throw e;\n}","preventionTips":["Pin the automation browser's UI language to English so /^withdraw/i and 'Sent X ago' regexes match.","Disable DOM-altering extensions in the automation profile.","Increase the post-navigation wait if cards render lazily, so parsing sees fully hydrated cards.","After LinkedIn UI updates, re-verify selectors on the invitation-manager page and update the extraction script promptly."],"tags":["linkedin","scraping","dom-parsing","ui-change","data-quality"],"backgroundTag":"dom-structure-changed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}