Skip to content

feat(apify): profile-level fallback for tweetless X accounts#756

Closed
sweetmantech wants to merge 1 commit into
mainfrom
feat/x-tweetless-profile-fallback
Closed

feat(apify): profile-level fallback for tweetless X accounts#756
sweetmantech wants to merge 1 commit into
mainfrom
feat/x-tweetless-profile-fallback

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

X profile stats ride on tweet items (items[0].author), so an account with zero recent tweets succeeds on Apify with an empty dataset and persists nothing — the connected socials row stays stale forever. This adds a fallback: on an empty tweet dataset, recover the requested handle from the run INPUT and start a profile-level scrape (apidojo/twitter-user-scraper), whose results persist through the same webhook receiver.

Tracking issue: recoupable/chat#1851 (optional PR #3 — independent of api#755 and database#40).

What changed

  • lib/apify/twitter/startTwitterUserScraping.ts (new) — starts apidojo/twitter-user-scraper (resolved actor id V38PZzpEgOfeeWvZY) with {twitterHandles: [handle], maxItems: 1} + the shared webhooks.
  • lib/apify/twitter/handleTwitterUserScraperResults.ts (new) — persists the profile; reads the run INPUT to recover the requested handle and filters out the related-user padding the actor appends (verified live: a 1-handle request returned the user plus 9 similar accounts).
  • lib/apify/twitter/handleTwitterProfileScraperResults.ts — empty-dataset branch: read INPUT handle → start the fallback → return { social: null, fallbackRunId }. No-ops (unchanged behavior) when the payload has no key-value store id or the INPUT has no handle.
  • lib/apify/getApifyResultHandler.ts — registry entry for V38PZzpEgOfeeWvZY.
  • lib/apify/validateApifyWebhookRequest.ts — schema now passes through the optional resource.defaultKeyValueStoreId (present on Apify's default payload; previously stripped by zod).

Verified live (2026-07-06, real Apify runs — not simulated)

Claim Evidence
Actor id resolves GET /v2/acts/apidojo~twitter-user-scraperV38PZzpEgOfeeWvZY
Output shape + padding behavior live run with twitterHandles: ["ashnikko"] → requested user (followers: 239289, url: https://x.com/ashnikko) plus 9 related users → hence the handle filter
twitterHandles input accepted HTTP 201, run SUCCEEDED
Tweetless fixture behaves as described the real handle that motivated this returns 0 items from the tweet actor; note it also returns 0 from the user scraper even with includeUnavailableUsers: true — that account appears deleted/renamed, so for it specifically no scraper can help; the fallback covers accounts that exist but don't tweet

Tests (RED→GREEN)

New/updated suites failed before implementation (2 assertion failures + 2 module-not-found), pass after. vitest run lib/apify lib/socials128 tests passed. tsc --noEmit: 200 pre-existing errors on clean origin/test, 200 after — none new. eslint clean.

Pending preview verification

End-to-end webhook chain (empty tweet run → fallback run → profile persisted to the connected row) needs a preview/test-deployment pass with a real tweetless-but-live handle; will post the results table here.

🤖 Generated with Claude Code


Summary by cubic

Adds a profile-level fallback for tweetless X accounts so follower and bio stats still update. If the tweet scraper returns no items, we start apidojo/twitter-user-scraper for the handle and persist the profile via a new handler.

  • New Features
    • Empty dataset fallback: read the handle from the run INPUT (via resource.defaultKeyValueStoreId) and start apidojo/twitter-user-scraper with maxItems: 1 using shared webhooks.
    • Results handling: new handler filters out related-user padding, normalizes the x.com profile URL, and upserts only the requested handle’s profile to socials.
    • Wiring: register actor id V38PZzpEgOfeeWvZY in the result-handler map.
    • Webhook schema: accept optional resource.defaultKeyValueStoreId to read the run INPUT; no-op when missing or when the INPUT has no handle.

Written for commit 3a63874. Summary will update on new commits.

Review in cubic

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 6, 2026 7:32pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0c62e760-071e-4f69-94a5-a1e78a3cf4b3

📥 Commits

Reviewing files that changed from the base of the PR and between ded19d4 and 3a63874.

⛔ Files ignored due to path filters (4)
  • lib/apify/__tests__/getApifyResultHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/apify/twitter/__tests__/handleTwitterProfileScraperResults.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/apify/twitter/__tests__/handleTwitterUserScraperResults.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/apify/twitter/__tests__/startTwitterUserScraping.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/apify/getApifyResultHandler.ts
  • lib/apify/twitter/handleTwitterProfileScraperResults.ts
  • lib/apify/twitter/handleTwitterUserScraperResults.ts
  • lib/apify/twitter/startTwitterUserScraping.ts
  • lib/apify/validateApifyWebhookRequest.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/x-tweetless-profile-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Confidence score: 5/5

  • In lib/apify/twitter/__tests__/startTwitterUserScraping.test.ts, the riskiest gap is missing coverage for handles with surrounding whitespace, so a subtle parsing regression could slip through and send an untrimmed value in the API call — add the `
  • KETTAMA_
  • style test case before merging to confirm it resolves totwitterHandles: ["KETTAMA_"]`.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/apify/twitter/__tests__/startTwitterUserScraping.test.ts">

<violation number="1" location="lib/apify/twitter/__tests__/startTwitterUserScraping.test.ts:15">
P3: Add a test case for a handle with surrounding whitespace (e.g., `" KETTAMA_ "`) to verify `trim()` handles the edge case correctly — that it resolves to `twitterHandles: ["KETTAMA_"]` in the API call rather than keeping the whitespace or rejecting. The `"  "` case only validates the empty-after-trim guard, not the trim-and-pass path.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Webhook as Apify Webhook
    participant Validator as validateApifyWebhookRequest
    participant Router as getApifyResultHandler
    participant TweetHandler as handleTwitterProfileScraperResults
    participant KVStore as Apify Key-Value Store
    participant FallbackStarter as startTwitterUserScraping
    participant UserHandler as handleTwitterUserScraperResults
    participant Dataset as Apify Dataset
    participant DB as Supabase socials

    Note over Webhook,DB: Tweet-based scrape webhook fires (empty dataset)

    Webhook->>Validator: POST webhook payload
    Validator->>Validator: Parse & validate (zod now passes defaultKeyValueStoreId)
    Validator-->>Router: Parsed payload (actorId: nfp1fpt5gUlBwPcor)
    Router->>Router: Lookup handler → handleTwitterProfileScraperResults
    Router-->>TweetHandler: Route to handler

    TweetHandler->>Dataset: listItems() on defaultDatasetId
    Dataset-->>TweetHandler: [] (empty - tweetless account)

    alt Empty dataset fallback
        TweetHandler->>TweetHandler: Check defaultKeyValueStoreId exists
        alt KV store ID present
            TweetHandler->>KVStore: getRecord("INPUT")
            KVStore-->>TweetHandler: { twitterHandles: ["KETTAMA_"] }
            TweetHandler->>TweetHandler: Extract handle from INPUT
            TweetHandler->>FallbackStarter: start("KETTAMA_")
            FallbackStarter->>FallbackStarter: Start apidojo/twitter-user-scraper with {twitterHandles, maxItems:1}
            FallbackStarter-->>TweetHandler: { runId, datasetId }
            TweetHandler-->>Webhook: { social: null, fallbackRunId }
        else No KV store ID or no handle in INPUT
            TweetHandler-->>Webhook: { social: null } (no felback)
        end
    end

    Note over Webhook,DB: Fallback user-scraper webhook fires

    Webhook->>Validator: POST webhook payload (actorId: V38PZzpEgOfeeWvZY)
    Validator-->>Router: Parsed payload
    Router->>Router: Lookup handler → handleTwitterUserScraperResults
    Router-->>UserHandler: Route to handler

    UserHandler->>KVStore: getRecord("INPUT") on defaultKeyValueStoreId
    KVStore-->>UserHandler: { twitterHandles: ["KETTAMA_"] }
    
    UserHandler->>Dataset: listItems() on defaultDatasetId
    Dataset-->>UserHandler: [padded related users..., requested user]

    UserHandler->>UserHandler: Filter dataset to requested handle (case-insensitive match)

    alt Requested user found
        UserHandler->>UserHandler: Build social object (lowercase profile_url)
        UserHandler->>DB: upsertSocials([social])
        DB-->>UserHandler: Updated row
        UserHandler-->>Webhook: { social }
    else Requested user not in dataset
        UserHandler-->>Webhook: { social: null }
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


describe("startTwitterUserScraping", () => {
it("starts the profile-level user scraper for one handle", async () => {
const r = await startTwitterUserScraping("KETTAMA_");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Add a test case for a handle with surrounding whitespace (e.g., " KETTAMA_ ") to verify trim() handles the edge case correctly — that it resolves to twitterHandles: ["KETTAMA_"] in the API call rather than keeping the whitespace or rejecting. The " " case only validates the empty-after-trim guard, not the trim-and-pass path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/apify/twitter/__tests__/startTwitterUserScraping.test.ts, line 15:

<comment>Add a test case for a handle with surrounding whitespace (e.g., `" KETTAMA_ "`) to verify `trim()` handles the edge case correctly — that it resolves to `twitterHandles: ["KETTAMA_"]` in the API call rather than keeping the whitespace or rejecting. The `"  "` case only validates the empty-after-trim guard, not the trim-and-pass path.</comment>

<file context>
@@ -0,0 +1,28 @@
+
+describe("startTwitterUserScraping", () => {
+  it("starts the profile-level user scraper for one handle", async () => {
+    const r = await startTwitterUserScraping("KETTAMA_");
+    expect(apifyClient.actor).toHaveBeenCalledWith("apidojo/twitter-user-scraper");
+    expect(start).toHaveBeenCalledWith(
</file context>

@sweetmantech sweetmantech changed the base branch from test to main July 6, 2026 19:25
@sweetmantech sweetmantech force-pushed the feat/x-tweetless-profile-fallback branch from 1d8e76a to 27563d2 Compare July 6, 2026 19:25
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

apidojo/twitter-scraper-lite carries profile stats on tweet items, so an
X account with zero recent tweets never persists followers/bio — its run
succeeds with an empty dataset and the connected socials row stays stale
(recoupable/chat#1851, observed for a real artist handle). On an empty
dataset the tweet handler now recovers the requested handle from the run
INPUT and starts apidojo/twitter-user-scraper (V38PZzpEgOfeeWvZY); a new
handler persists the profile, filtering out the related-user padding the
actor appends (shape verified on a live run 2026-07-06).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sweetmantech sweetmantech force-pushed the feat/x-tweetless-profile-fallback branch from 27563d2 to 3a63874 Compare July 6, 2026 19:31
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Closing unmerged — deferred on a YAGNI call (sweetman, 2026-07-06): no current artist on the platform is in the live-but-tweetless population this covers (the motivating fixture turned out to be a deleted/renamed account, which no scraper can help). The core chat#1851 bug is fully fixed by api#755 + database#40. Revisit if a specific artist hits this: the branch (feat/x-tweetless-profile-fallback) carries a verified design — actor apidojo/twitter-user-scraper (V38PZzpEgOfeeWvZY), related-user padding filter, run-INPUT handle recovery — with unit tests green (128) and live-verified actor shapes. Known cost of deferring: a tweetless account's scrape bills credits and persists nothing, silently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant