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

@sweetmantechsweetmantech commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Part of recoupable/app#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/app#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

vercelBot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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

ProjectDeploymentActionsUpdated (UTC)
apiReadyReadyPreviewJul 1, 2026 11:43pm

Request Review

@coderabbitai

coderabbitaiBot 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-aicubic-dev-aiBot 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(typeofhandler!=="function"){

…-IG platform)
Per review, the registry alone wasn't worth shipping — wire one new platform
through the full persistence path (recoupable/app#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>
@sweetmantechsweetmantech changed the title refactor(apify): registry webhook dispatch (foundation for non-IG scrape persistence)feat(apify): registry webhook dispatch + LinkedIn persistence end-to-endJul 1, 2026

@cubic-dev-aicubic-dev-aiBot 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
ContributorAuthor

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:

fieldbeforeafter (written by the webhook @ 23:46:46)
followerCount013,203
bionull"I am the builder. I code, architect, and scale the system…"
avatarnullhttps://media.licdn.com/dms/image/v2/C4D03AQ…
regionnullColumbus, 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 mainJul 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/app#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 freeto 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

@sweetmantech