Skip to content

feat(apify): LinkedIn scrape returns posts when posts is requested#745

Merged
sweetmantech merged 1 commit into
mainfrom
feat/linkedin-posts-scrape
Jul 2, 2026
Merged

feat(apify): LinkedIn scrape returns posts when posts is requested#745
sweetmantech merged 1 commit into
mainfrom
feat/linkedin-posts-scrape

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements recoupable/chat#1838. Contract: recoupable/docs#260 — merge order docs → api. Base is main per the current direct-to-main flow; I'll sync test after merge.

One-file change: startLinkedinProfileScraping(handle, posts?) — when posts is defined, runs harvestapi/linkedin-profile-posts with {targetUrls: [url], maxPosts: posts}; when omitted, the existing profile actor with byte-identical input (unit-tested). Handle guards (in/company/school legacy rows), full-URL passthrough, webhooks, and error handling unchanged and re-asserted for the posts branch.

This closes the last platform gap in the posts param: LinkedIn post items carry engagement (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):

  • posts=20 → posts actor + {targetUrls, maxPosts: 20}
  • posts branch keeps full-URL passthrough + invalid-handle guards
  • posts omitted → profile actor, byte-identical legacy input

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 7478135671589634048 shows comments: 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.

  • New Features
    • startLinkedinProfileScraping(handle, posts?) selects harvestapi/linkedin-profile-posts with { targetUrls: [url], maxPosts: posts } when posts is set.
    • When posts is omitted, uses harvestapi/linkedin-profile-scraper with { urls: [url] }.

Written for commit fabbd0a. Summary will update on new commits.

Review in cubic

…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>
@vercel

vercel Bot commented Jul 2, 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 2, 2026 6:50pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 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: 55 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: 0ced5dfe-1e9a-4038-873e-e96d693142d8

📥 Commits

Reviewing files that changed from the base of the PR and between f45cd0f and fabbd0a.

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

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.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification (2026-07-02) ✅

Preview: api-18607hcox-recoup.vercel.app (resolved by PR-head sha). Caller: preview-env account, balance read around every call.

Check (Done-when) Expected Actual Result
Default LinkedIn scrape profile actor, byte-identical legacy, −5 credits 200 (run qgSLMnt5ZQNISuk1I) → 1 profile item (headline/followerCount: 13220, zero post fields), balance −5 exactly
LinkedIn ?posts=5 posts actor, 5 post items w/ engagement, −10 credits (5+5) 200 (run v11RPZDj0Vfo19PbP) → 5 post items, each with engagement.likes/comments/shares + reaction breakdown, balance −10 exactly
Pinned check: post 7478135671589634048 shows comments: 1 present w/ comment count present, comments: 1 ✓ (matched on engagement.id)

Bonus evidence the posts actor beats the connector: the dataset surfaced a 12-like / 10-comment / 1-share post that the LINKEDIN_LIST_REACTIONS path had never shown (personal-post comments/shares are invisible to the connector).

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 test after the direct-to-main merge.

@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 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 webhook actorId handler 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
Loading

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

Re-trigger cubic

const actorId =
posts === undefined
? "harvestapi/linkedin-profile-scraper"
: "harvestapi/linkedin-profile-posts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@sweetmantech sweetmantech merged commit 21ee50b into main Jul 2, 2026
6 checks passed
sweetmantech added a commit that referenced this pull request Jul 2, 2026
sweetmantech added a commit to recoupable/skills that referenced this pull request Jul 2, 2026
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>
sweetmantech added a commit to recoupable/skills that referenced this pull request Jul 2, 2026
…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>
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