Skip to content

feat(apify): registry webhook dispatch + LinkedIn persistence end-to-end#738

Merged
sweetmantech merged 3 commits into
mainfrom
feat/persist-scrape-results
Jul 1, 2026
Merged

feat(apify): registry webhook dispatch + LinkedIn persistence end-to-end#738
sweetmantech merged 3 commits into
mainfrom
feat/persist-scrape-results

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Part of recoupable/chat#1833 (data freshness). Base: main.

Problem

Scrape results for every non-Instagram platform never persist to socials — they're only retrievable via the poll endpoint. Two root causes (both verified):

  1. apifyWebhookHandler routed only the two Instagram actor ids via a hardcoded switch.
  2. Deeper: webhooks are registered per-run, and only the Instagram start modules attach getApifyWebhooks() — other platforms' runs never call /api/apify at all.

This PR

Registry foundation:

  • getApifyResultHandler(actorId) — actorId → handler registry (SRP, own file + tests), replacing the switch. Behavior-preserving for IG.

LinkedIn persisted end-to-end (first non-IG platform, per review — structure alone wasn't worth shipping):

  • startLinkedinProfileScraping attaches getApifyWebhooks() to the run.
  • New handleLinkedinProfileScraperResults: upserts the scraped profile into socials (keyed profile_url) — username/avatar/bio/followerCount/region. Field mapping from a real harvestapi run (TNwgXpQmhvoCHsKg8, 2026-07-01), including the {element, error, status} wrapper for missing profiles (skipped, no write).
  • Registry entry: LpVuK3Zozwuipa5bp (harvestapi/linkedin-profile-scraper — id resolved from the Apify API) → handler.

Tests (TDD)

RED→GREEN: handler (real profile shape → exact upsert; 404-wrapper → no write; empty dataset → no-op), registry mapping, webhook attachment on start. Full lib/apify suite green (54); tsc + eslint clean.

Remaining follow-ups (documented in the registry's doc comment + chat#1833)

TikTok/X/YouTube/Threads/Facebook stay poll-only until each gets the same two-line webhook attachment + a handler built from its real output shape (captured samples for TikTok/X/YouTube exist in chat#1833).

🤖 Generated with Claude Code

…IG persistence)

apifyWebhookHandler routed only the two Instagram actors via a hardcoded switch;
every other platform (TikTok/X/YouTube/Threads/Facebook, and the new LinkedIn) hits
"unhandled actorId" and persists NOTHING — those scrapes return data on the poll
endpoint but never write back to socials (recoupable/chat#1833).

Extract getApifyResultHandler (actorId → handler registry, SRP) and dispatch through
it, so adding a platform is a single registry entry instead of another switch arm.
Behavior-preserving — all existing webhook tests pass unchanged. The per-platform
handlers (mirror handleInstagramProfileScraperResults) are the documented follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 1, 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 1, 2026 11:43pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 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: 23 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: 788cc504-898b-466c-8fa5-621964642655

📥 Commits

Reviewing files that changed from the base of the PR and between 3bedb9d and 223752f.

⛔ Files ignored due to path filters (4)
  • lib/apify/__tests__/apifyWebhookHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/apify/__tests__/getApifyResultHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/apify/__tests__/startLinkedinProfileScraping.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/apify/linkedin/__tests__/handleLinkedinProfileScraperResults.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (4)
  • lib/apify/apifyWebhookHandler.ts
  • lib/apify/getApifyResultHandler.ts
  • lib/apify/linkedin/handleLinkedinProfileScraperResults.ts
  • lib/apify/linkedin/startLinkedinProfileScraping.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/persist-scrape-results

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 3 files

Confidence score: 5/5

  • In lib/apify/apifyWebhookHandler.ts, the registry lookup is only checked for truthiness, so special actor IDs like __proto__ can be treated as handlers and throw an internal exception instead of returning the expected unhandled-actor path; this is a narrow edge case but can produce incorrect error behavior if merged as-is — add a callable/type guard (e.g., verify the resolved value is a function) before invoking it.
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/apifyWebhookHandler.ts">

<violation number="1" location="lib/apify/apifyWebhookHandler.ts:20">
P3: Guard that the registry result is actually callable, not just truthy. Otherwise special actor ids such as `__proto__` produce an internal handler exception instead of the intended unhandled-actor response.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Apify as Apify Platform
    participant Webhook as apifyWebhookHandler (POST /api/apify)
    participant Validator as validateApifyWebhookRequest
    participant Registry as getApifyResultHandler
    participant IGProfile as handleInstagramProfileScraperResults
    participant IGComments as handleInstagramCommentsScraper
    participant DB as Database (socials / posts)

    Note over Apify,DB: NEW: dispatch via actorId registry instead of switch

    Apify->>Webhook: POST webhook payload (actorId, eventData)

    Webhook->>Validator: validateApifyWebhookRequest(request)
    alt Invalid payload
        Validator-->>Webhook: error response
        Webhook-->>Apify: 400 error (unchanged)
    else Valid payload
        Validator-->>Webhook: validated payload { actorId, ... }
        Webhook->>Registry: getApifyResultHandler(actorId)
        Registry-->>Webhook: ApifyResultHandler | undefined

        alt Known actorId (Instagram profile/comments)
            Note over Registry,IGProfile: e.g. actorId = "dSCLg0C3YEZ83HzYX"
            Webhook->>IGProfile: handler(validated payload)
            IGProfile->>DB: upsert socials + posts
            DB-->>IGProfile: success
            IGProfile-->>Webhook: result object
            Webhook-->>Apify: 200 { status: "ok", ... }

        else Known actorId (Instagram comments)
            Note over Registry,IGComments: actorId = "SbK00X0JYCPblD2wp"
            Webhook->>IGComments: handler(validated payload)
            IGComments->>DB: upsert comments data
            DB-->>IGComments: success
            IGComments-->>Webhook: result object
            Webhook-->>Apify: 200 { status: "ok", ... }

        else Unknown actorId (unhandled)
            Note over Registry,Webhook: any other actorId → undefined handler
            Webhook->>Webhook: log warning, return "Unhandled actorId"
            Webhook-->>Apify: 200 { status: "error", error: "Unhandled actorId: ..." }
        end
    end

    Note over Webhook: Always returns 200 to Apify (prevents retries)
Loading

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

Re-trigger cubic

const { actorId } = validated.eventData;

const handler = getApifyResultHandler(actorId);
if (!handler) {

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: Guard that the registry result is actually callable, not just truthy. Otherwise special actor ids such as __proto__ produce an internal handler exception instead of the intended unhandled-actor response.

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

<comment>Guard that the registry result is actually callable, not just truthy. Otherwise special actor ids such as `__proto__` produce an internal handler exception instead of the intended unhandled-actor response.</comment>

<file context>
@@ -20,23 +16,18 @@ export async function apifyWebhookHandler(request: NextRequest): Promise<NextRes
   const { actorId } = validated.eventData;
 
+  const handler = getApifyResultHandler(actorId);
+  if (!handler) {
+    console.warn(`[WARN] apifyWebhookHandler: unhandled actorId ${actorId}`);
+    return NextResponse.json(
</file context>
Suggested change
if (!handler) {
if (typeof handler !== "function") {

…-IG platform)

Per review, the registry alone wasn't worth shipping — wire one new platform
through the full persistence path (recoupable/chat#1833):
- startLinkedinProfileScraping now attaches getApifyWebhooks() to the run — the
  root cause of "nothing but IG persists" is that only the Instagram start
  modules registered the per-run webhook; every other platform's runs never
  called /api/apify at all.
- New handleLinkedinProfileScraperResults: upserts the scraped profile into
  socials (keyed on profile_url) — username/avatar/bio/followerCount/region.
  Field mapping taken from a REAL harvestapi run (TNwgXpQmhvoCHsKg8), including
  the {element,error,status} wrapper shape for missing profiles (skipped).
- Registry: LpVuK3Zozwuipa5bp (harvestapi/linkedin-profile-scraper, id resolved
  from the Apify API) → the new handler.

TikTok/X/YouTube/Threads/Facebook remain poll-only — each needs the same
webhook attachment + a handler with its real output shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sweetmantech sweetmantech changed the title refactor(apify): registry webhook dispatch (foundation for non-IG scrape persistence) feat(apify): registry webhook dispatch + LinkedIn persistence end-to-end Jul 1, 2026

@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 7 files (changes from recent commits).

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/linkedin/handleLinkedinProfileScraperResults.ts">

<violation number="1" location="lib/apify/linkedin/handleLinkedinProfileScraperResults.ts:43">
P2: LinkedIn webhook persistence can fail for valid-looking items that include `linkedinUrl` but omit `publicIdentifier`, because the inserted `socials` row has no required `username`. Falling back to the handle parsed from `linkedinUrl` would keep the upsert valid and consistent with scrape startup.</violation>
</file>

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

Re-trigger cubic


const social = {
profile_url: normalizeProfileUrl(first.linkedinUrl),
username: first.publicIdentifier,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: LinkedIn webhook persistence can fail for valid-looking items that include linkedinUrl but omit publicIdentifier, because the inserted socials row has no required username. Falling back to the handle parsed from linkedinUrl would keep the upsert valid and consistent with scrape startup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/apify/linkedin/handleLinkedinProfileScraperResults.ts, line 43:

<comment>LinkedIn webhook persistence can fail for valid-looking items that include `linkedinUrl` but omit `publicIdentifier`, because the inserted `socials` row has no required `username`. Falling back to the handle parsed from `linkedinUrl` would keep the upsert valid and consistent with scrape startup.</comment>

<file context>
@@ -0,0 +1,52 @@
+
+  const social = {
+    profile_url: normalizeProfileUrl(first.linkedinUrl),
+    username: first.publicIdentifier,
+    avatar: first.photo ?? null,
+    bio: first.about ?? first.headline ?? null,
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — LinkedIn persistence works end-to-end (webhook → DB write)

Tested on this PR's preview (https://api-3losk19sr-recoup.vercel.app, head 223752f6) against a real LinkedIn social (social_id c8e25612…, linkedin.com/in/sweetmaneth).

The definitive check — the webhook wrote the DB row, not a human

Captured the socials row before the scrape, fired one POST /api/socials/{id}/scrape, and re-read the row after the run succeeded:

field before after (written by the webhook @ 23:46:46)
followerCount 0 13,203
bio null "I am the builder. I code, architect, and scale the system…"
avatar null https://media.licdn.com/dms/image/v2/C4D03AQ…
region null Columbus, Ohio, United States

Full chain, verified live: POST /api/socials/{id}/scrape200 {"runId":"rgJsSe0By69c7tShB"} → run SUCCEEDEDApify POSTed ACTOR.RUN.SUCCEEDED to the preview's /api/apifygetApifyResultHandler("LpVuK3Zozwuipa5bp")handleLinkedinProfileScraperResultsupsertSocials keyed on profile_url. No manual DB writes anywhere in the loop.

Why nothing but IG ever persisted (root cause, confirmed)

Webhooks are attached per-run, and only the Instagram start modules passed { webhooks: getApifyWebhooks() } — every other platform's runs never called /api/apify at all. The switch was only the second half of the gap. This PR fixes both for LinkedIn: webhook attachment on start + handler + registry entry.

Also verified

  • Registry preserves IG behavior (all existing webhook tests pass unchanged).
  • The 404-wrapper shape ({element, error, status} — captured from a real missing-profile run) is skipped with no write.
  • Full lib/apify suite green (54); tsc + eslint clean.

Remaining follow-ups (documented in the registry doc comment + chat#1833)

TikTok/X/YouTube/Threads/Facebook stay poll-only until each gets the same two-part treatment (webhook attachment + handler from its real output shape — captured samples for TikTok/X/YouTube are in chat#1833).

@sweetmantech sweetmantech merged commit bd1958c into main Jul 1, 2026
6 checks passed
sweetmantech added a commit that referenced this pull request Jul 1, 2026
sweetmantech added a commit that referenced this pull request Jul 2, 2026
Completes the persistence work started in #738 (registry + LinkedIn):
TikTok, X/Twitter, YouTube, Threads, and Facebook scrapes now write back to
socials instead of being poll-only (recoupable/chat#1833).

Per platform (mirroring lib/apify/linkedin/):
- start module attaches getApifyWebhooks() to the run (the root cause of
  "nothing persists" — only IG/LinkedIn runs ever called /api/apify).
- handle<Platform>ProfileScraperResults upserts socials (keyed profile_url),
  with the field mapping taken from a REAL actor run of each platform:
  tiktok authorMeta (run G4YRI0e…), twitter author (ALVMZYX…), youtube
  channel fields (H0ZrIAs…), threads profile (9iiG1sD…), facebook page
  (ICZxdJB…).
- registry entry under the actor id resolved from the Apify API.

Round-trip hazards handled explicitly: twitter URLs are lowercased (actor
echoes display casing, stored rows are lowercase — upsert would duplicate);
youtube keys on inputChannelUrl (the URL we passed) because the actor's own
channelUrl is the /channel/UC… form which never matches the stored @handle.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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