feat(apify): LinkedIn scrape returns posts when posts is requested#745
Conversation
…ested
A requested posts depth switches the LinkedIn scrape from
harvestapi/linkedin-profile-scraper (whose dataset has zero post fields)
to harvestapi/linkedin-profile-posts with {targetUrls, maxPosts} — post
items with engagement counts (likes, comments, shares, reaction
breakdown). posts omitted keeps the profile actor with byte-identical
input. Same handle guards, URL normalization, webhooks, and error paths.
Matches X/YouTube semantics (depth → post items; omitted → snapshot).
Actor verified live 2026-07-02: run aKsHPucJ3thxuosSd, 5 posts @ $0.01005
($0.002/post) — the shipped 5 + posts credit rule covers it at ~5x margin,
no billing change.
Implements recoupable/chat#1838; contract in recoupable/docs#260.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 55 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Preview verification (2026-07-02) ✅Preview:
Bonus evidence the posts actor beats the connector: the dataset surfaced a 12-like / 10-comment / 1-share post that the Billing confirmed unchanged: deductions follow the shipped 5 + posts rule exactly; actor cost at this depth ≈ $0.01 (verified against list pricing on the #1838 evaluation run). Docs ↔ API ↔ live agree. Merge order: docs#260 → this; I'll sync |
There was a problem hiding this comment.
1 issue found across 2 files
Confidence score: 2/5
- In
lib/apify/linkedin/startLinkedinProfileScraping.ts, post-requested runs can be launched on an actor that is not included in the webhookactorIdhandler map, so runs may complete without any result processing and users can receive missing/empty outcomes. Wire the posts actorId to the appropriate webhook processing path (and verify with a posts-enabled run) before merging.
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/startLinkedinProfileScraping.ts">
<violation number="1" location="lib/apify/linkedin/startLinkedinProfileScraping.ts:39">
P1: Posts-requested LinkedIn runs can finish without result processing because they are started on a different actor that is not registered in the webhook actorId handler map. Consider wiring the posts actorId to an appropriate LinkedIn results handler (or adding a dedicated posts handler) so `posts` scrapes persist/process like other platforms.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Caller as API / Caller
participant StartScrape as startLinkedinProfileScraping
participant ApifyClient as Apify Client
participant ActorProfile as harvestapi/...profile-scraper
participant ActorPosts as harvestapi/...profile-posts
Note over StartScrape: Handle validation, URL construction
Caller->>StartScrape: startLinkedinProfileScraping(handle, posts?)
StartScrape->>StartScrape: trim, strip @, guard invalid handles
alt Invalid handle (in/company/school)
StartScrape-->>Caller: throw Error
else Valid handle
StartScrape->>StartScrape: build targetUrl (pass full URL or prepend in/)
alt posts === undefined (legacy profile snapshot)
StartScrape->>ApifyClient: actor("harvestapi/linkedin-profile-scraper")
ApifyClient-->>StartScrape: actor reference
StartScrape->>ActorProfile: start({ urls: [targetUrl] }, { webhooks })
ActorProfile-->>StartScrape: { runId, datasetId }
else posts !== undefined (NEW: requested posts depth)
StartScrape->>ApifyClient: actor("harvestapi/linkedin-profile-posts")
ApifyClient-->>StartScrape: actor reference
StartScrape->>ActorPosts: start({ targetUrls: [targetUrl], maxPosts: posts }, { webhooks })
ActorPosts-->>StartScrape: { runId, datasetId }
end
StartScrape-->>Caller: ApifyRunInfo
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const actorId = | ||
| posts === undefined | ||
| ? "harvestapi/linkedin-profile-scraper" | ||
| : "harvestapi/linkedin-profile-posts"; |
There was a problem hiding this comment.
P1: Posts-requested LinkedIn runs can finish without result processing because they are started on a different actor that is not registered in the webhook actorId handler map. Consider wiring the posts actorId to an appropriate LinkedIn results handler (or adding a dedicated posts handler) so posts scrapes persist/process like other platforms.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/apify/linkedin/startLinkedinProfileScraping.ts, line 39:
<comment>Posts-requested LinkedIn runs can finish without result processing because they are started on a different actor that is not registered in the webhook actorId handler map. Consider wiring the posts actorId to an appropriate LinkedIn results handler (or adding a dedicated posts handler) so `posts` scrapes persist/process like other platforms.</comment>
<file context>
@@ -23,12 +33,15 @@ const startLinkedinProfileScraping = async (handle: string): Promise<ApifyRunInf
+ const actorId =
+ posts === undefined
+ ? "harvestapi/linkedin-profile-scraper"
+ : "harvestapi/linkedin-profile-posts";
+ const input =
+ posts === undefined ? { urls: [targetUrl] } : { targetUrls: [targetUrl], maxPosts: posts };
</file context>
One bulk scrape now covers all four platforms: LinkedIn posts=N returns post items with engagement (likes, comments, shares, reaction breakdown) via harvestapi/linkedin-profile-posts — richer than the connector, which can't see personal-post comments/shares. LINKEDIN_LIST_REACTIONS is demoted to its remaining real use: pulling reactor identities (the warm-lead list on comment-gated posts). Shipped to prod 2026-07-02 (recoupable/api#745, tracker chat#1838). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he X connector (#72) * feat(ship-posts): measure via the bulk social scrape, not the X connector Step 1 now leads with POST /api/artist/socials/scrape?posts=N — one call returns per-post metrics for X (incl. viewCount impressions), YouTube (videos + Shorts), and Instagram (latestPosts); LinkedIn stays on LINKEDIN_LIST_REACTIONS (its scrape is profile-only). Documents the per-platform dataset shapes, the 5 + posts per-profile credit cost, the profile-linking setup (PATCH profileUrls), and the gotchas learned shipping it: X depth counts timeline items incl. RTs, IG public plays vs insights views, ~1h token burst rule. Replaces the dead X-connector guidance — TWITTER_GET_POST_ANALYTICS 403s (client-not-enrolled) and post lookup 401s; verified 2026-07-02. Backed by recoupable/api#741 + #744 (shipped to prod 2026-07-02; tracker: recoupable/chat#1836). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ship-posts): LinkedIn measurement via the scrape too (api#745) One bulk scrape now covers all four platforms: LinkedIn posts=N returns post items with engagement (likes, comments, shares, reaction breakdown) via harvestapi/linkedin-profile-posts — richer than the connector, which can't see personal-post comments/shares. LINKEDIN_LIST_REACTIONS is demoted to its remaining real use: pulling reactor identities (the warm-lead list on comment-gated posts). Shipped to prod 2026-07-02 (recoupable/api#745, tracker chat#1838). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ship-posts): KISS the measurement section — remove connector-era prose Net reduction vs main. Removed outdated/historical text rather than stacking additions: the timestamp_ms=id>>22 trick (scrape returns postedAt), the why-the-X-connector-fails explanation (one clause now), the LinkedIn personal-post limitation rationale and company-page reconnect note (justified a workaround we no longer use), and the full LINKEDIN_LIST_REACTIONS curl block (Step 4 already teaches connector calls; one gotcha line names the slug + entity format for the who-reacted lead-list case). Table collapsed to one column per platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Implements recoupable/chat#1838. Contract: recoupable/docs#260 — merge order docs → api. Base is
mainper the current direct-to-main flow; I'll synctestafter merge.One-file change:
startLinkedinProfileScraping(handle, posts?)— whenpostsis defined, runsharvestapi/linkedin-profile-postswith{targetUrls: [url], maxPosts: posts}; when omitted, the existing profile actor with byte-identical input (unit-tested). Handle guards (in/company/schoollegacy rows), full-URL passthrough, webhooks, and error handling unchanged and re-asserted for the posts branch.This closes the last platform gap in the
postsparam: LinkedIn post items carryengagement(likes, comments, shares, reaction-type breakdown) — richer than the connector, which can't see comments/shares on personal posts and has no list-my-posts endpoint at all.Billing: no change needed (per the #1838 evaluation). Actor pricing $0.002/post + $0.00005 start, verified on live run
aKsHPucJ3thxuosSd(5 posts → $0.01005, exactly list). Under the shipped 5 + posts rule: posts=100 → ~$0.20 cost vs $1.05 charged (~5.2× margin); YouTube remains the binding worst case.Tests (TDD, red → green)
3 new cases written first and confirmed failing (2 RED before implementation):
{targetUrls, maxPosts: 20}lib/apify: 77/77. Full suite passes.tsc --noEmit: 200 pre-existing errors, none new. ESLint clean.Preview verification to follow as a PR comment (pinned check: post
7478135671589634048showscomments: 1).🤖 Generated with Claude Code
Summary by cubic
Adds optional posts depth to LinkedIn scraping: when provided, we return recent post items with engagement counts via
harvestapi/linkedin-profile-posts; when omitted, we keep the existing profile snapshot.startLinkedinProfileScraping(handle, posts?)selectsharvestapi/linkedin-profile-postswith{ targetUrls: [url], maxPosts: posts }whenpostsis set.postsis omitted, usesharvestapi/linkedin-profile-scraperwith{ urls: [url] }.Written for commit fabbd0a. Summary will update on new commits.