Skip to content

feat(apify): add LinkedIn profile scraping (harvestapi)#737

Merged
sweetmantech merged 5 commits into
mainfrom
feat/linkedin-scraper
Jul 1, 2026
Merged

feat(apify): add LinkedIn profile scraping (harvestapi)#737
sweetmantech merged 5 commits into
mainfrom
feat/linkedin-scraper

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Part of recoupable/chat#1833 (platform coverage). Base: main.

Why

LinkedIn was the only one of {YouTube, TikTok, LinkedIn, X, Instagram} unsupported by POST /api/socials/{social_id}/scrape — a linkedin.com profile returned 400 "Unsupported social media platform" (verified live). getSocialPlatformByLink also returned NONE for LinkedIn.

Fix

  • lib/apify/linkedin/startLinkedinProfileScraping.tsharvestapi/linkedin-profile-scraper (mirrors the tiktok/youtube modules). Ref: https://apify.com/harvestapi/linkedin-profile-scraper (+ harvestapi/linkedin-profile-posts for posts, follow-up).
  • scrapeProfileUrl.ts: dispatch linkedin.com → the new scraper.
  • getSocialPlatformByLink.ts: linkedin.comLINKEDIN.

Tests (TDD)

  • RED→GREEN getSocialPlatformByLink.test.ts (LINKEDIN) + scrapeProfileUrl.test.ts (dispatches linkedin.com; unsupported → null). tsc + eslint clean.

Follow-ups (noted, not in this PR)

  • Confirm the harvestapi input shape (profiles: [url]) against the actor on the preview.
  • Result persistence — the webhook only routes Instagram actors today; LinkedIn (and TikTok/X/YouTube) results won't persist until the companion persistence PR lands.

🤖 Generated with Claude Code


Summary by cubic

Adds LinkedIn profile scraping via harvestapi/linkedin-profile-scraper. The scrape API now supports LinkedIn; the scraper builds a profile URL from a handle or accepts a full URL and rejects invalid legacy handles.

  • New Features

    • Added startLinkedinProfileScraping using harvestapi/linkedin-profile-scraper.
    • Routed linkedin.com to the LinkedIn scraper and mapped it to LINKEDIN in getSocialPlatformByLink.
    • Added unit tests for routing, platform mapping, actor input, and LinkedIn handle parsing.
  • Bug Fixes

    • Guard against legacy LinkedIn usernames equal to in, company, or school; accept full profile URLs, trim spaces, and ignore a leading @.
    • Use the actor input key urls when starting runs.
    • getUsernameFromProfileUrl now extracts LinkedIn handles from /in, /company, and /school.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added support for LinkedIn profile scraping from profile links or handles.
    • LinkedIn profiles are now recognized as a supported social platform.
  • Bug Fixes
    • Improved input handling by trimming extra spaces and ignoring a leading @ in LinkedIn handles.
    • Added validation to prevent invalid or empty LinkedIn profile inputs from being submitted.
    • Better error handling when a scrape run fails or is aborted.

LinkedIn was the only one of {YouTube, TikTok, LinkedIn, X, Instagram} unsupported
by POST /api/socials/{social_id}/scrape — a linkedin.com profile returned 400
"Unsupported social media platform" (recoupable/chat#1833). Add:
- lib/apify/linkedin/startLinkedinProfileScraping.ts → harvestapi/linkedin-profile-scraper
  (mirrors the tiktok/youtube modules).
- scrapeProfileUrl: dispatch linkedin.com → the new scraper.
- getSocialPlatformByLink: linkedin.com → LINKEDIN (was NONE).

Note: harvestapi input shape (profiles: [url]) should be confirmed against the
actor on the preview; result persistence is handled in the companion webhook PR.

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:31pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 36 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: 6f9f516e-7cf7-4e33-a06b-96c302dcc5fb

📥 Commits

Reviewing files that changed from the base of the PR and between af9f2ac and b65c1d6.

⛔ Files ignored due to path filters (3)
  • lib/apify/__tests__/scrapeProfileUrl.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/socials/__tests__/getUsernameFromProfileUrl.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/apify/linkedin/startLinkedinProfileScraping.ts
  • lib/socials/getUsernameFromProfileUrl.ts
📝 Walkthrough

Walkthrough

Adds a new LinkedIn profile scraper module that invokes an Apify actor to scrape LinkedIn profiles, returning run/dataset identifiers. Wires this scraper into the existing platform-based scraper dispatch table and extends platform link detection to recognize LinkedIn URLs.

Changes

LinkedIn Scraping Integration

Layer / File(s) Summary
LinkedIn scraper implementation
lib/apify/linkedin/startLinkedinProfileScraping.ts
New module normalizes a handle/URL, calls the Harvest API LinkedIn profile scraper actor with { profiles: [targetUrl] }, returns { runId, datasetId } or null, throws on FAILED/ABORTED status, and logs/rethrows other errors.
Platform detection and routing wiring
lib/apify/scrapeProfileUrl.ts, lib/artists/getSocialPlatformByLink.ts
Imports the new scraper and registers a linkedin.com matcher in PLATFORM_SCRAPERS; getSocialPlatformByLink now returns "LINKEDIN" for matching URLs instead of "NONE".

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant scrapeProfileUrl
  participant startLinkedinProfileScraping
  participant ApifyActor

  Caller->>scrapeProfileUrl: profile URL
  scrapeProfileUrl->>scrapeProfileUrl: match linkedin.com in PLATFORM_SCRAPERS
  scrapeProfileUrl->>startLinkedinProfileScraping: handle/URL
  startLinkedinProfileScraping->>ApifyActor: run harvestapi/linkedin-profile-scraper
  ApifyActor-->>startLinkedinProfileScraping: run status, id, defaultDatasetId
  startLinkedinProfileScraping-->>scrapeProfileUrl: {runId, datasetId} or null/error
Loading

Poem

A rabbit hops onto LinkedIn's lawn,
Scraping profiles from dusk till dawn,
Handles trimmed, "@" swept away,
Actors run, datasets sway,
One more platform, wired and drawn! 🐇🔗

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning FAIL: startLinkedinProfileScraping is 29 lines and mixes normalization, URL building, Apify start, terminal-state checks, and logging. Split validation/URL building from Apify orchestration, remove terminal-state handling from the .start() path, and extract any shared handle-normalization helper.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/linkedin-scraper

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.

3 issues found across 5 files

Confidence score: 2/5

  • In lib/apify/linkedin/startLinkedinProfileScraping.ts, the actor payload uses profiles instead of the expected urls, so LinkedIn runs may ignore the requested profile and return incorrect/no data — switch the payload key to urls and verify with a real profile URL before merging.
  • In lib/apify/scrapeProfileUrl.ts, the /in/<slug> fallback can resolve to in when username is empty, which can scrape the wrong LinkedIn profile and propagate bad identity data — add LinkedIn-specific URL parsing (or guardrails for empty usernames) and cover it with a targeted test before merging.
  • In lib/artists/getSocialPlatformByLink.ts, raw linkedin.com substring matching can misclassify domains like notlinkedin.com, leading to incorrect platform routing downstream — tighten matching to hostname boundaries (or URL host parsing) to de-risk false positives.
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/artists/getSocialPlatformByLink.ts">

<violation number="1" location="lib/artists/getSocialPlatformByLink.ts:13">
P2: The LinkedIn detection matches any string containing `linkedin.com`, so unrelated URLs like `notlinkedin.com` can be misclassified as LinkedIn. Match the hostname boundary instead of using a raw substring check.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant API as POST /api/socials/{id}/scrape
    participant PlatformDetector as getSocialPlatformByLink
    participant ScraperDispatcher as scrapeProfileUrl
    participant LinkedInScraper as startLinkedinProfileScraping
    participant Apify as Apify Client (Actor API)
    participant HarvestAPI as harvestapi/linkedin-profile-scraper

    Note over Client,HarvestAPI: NEW: LinkedIn profile scraping flow

    Client->>API: POST with linkedin.com URL
    API->>PlatformDetector: getSocialPlatformByLink(url)
    PlatformDetector-->>API: "LINKEDIN" (NEW: linkedin.com detection)
    API->>ScraperDispatcher: scrapeProfileUrl(url, username)
    ScraperDispatcher->>ScraperDispatcher: Match linkedin.com → startLinkedinProfileScraping
    ScraperDispatcher->>LinkedInScraper: startLinkedInProfileScraping(handle)
    LinkedInScraper->>LinkedInScraper: Build input { profiles: [targetUrl] }
    LinkedInScraper->>Apify: actor("harvestapi/linkedin-profile-scraper").start(input)
    Apify->>HarvestAPI: Start LinkedIn scrape job (external API)
    HarvestAPI-->>Apify: Run object (id, defaultDatasetId, status)
    alt Run started successfully
        Apify-->>LinkedInScraper: run
        LinkedInScraper-->>ScraperDispatcher: { runId, datasetId }
        ScraperDispatcher-->>API: { supported: true, runId, datasetId }
        API-->>Client: 200 OK
    else Run failed or aborted
        Apify-->>LinkedInScraper: run with status FAILED/ABORTED
        LinkedInScraper->>LinkedInScraper: Throw OUTSTANDING_ERROR
        LinkedInScraper-->>ScraperDispatcher: error
        ScraperDispatcher-->>API: null / error
        API-->>Client: 400 / 500 error
    end

    Note over API,Client: Persistence of results not implemented in this PR (follow-up)
Loading

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

Re-trigger cubic

Comment thread lib/apify/linkedin/startLinkedinProfileScraping.ts Outdated
if (link.includes("instagram.com")) return "INSTAGRAM";
if (link.includes("spotify.com")) return "SPOTIFY";
if (link.includes("tiktok.com")) return "TIKTOK";
if (link.includes("linkedin.com")) return "LINKEDIN";

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: The LinkedIn detection matches any string containing linkedin.com, so unrelated URLs like notlinkedin.com can be misclassified as LinkedIn. Match the hostname boundary instead of using a raw substring check.

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

<comment>The LinkedIn detection matches any string containing `linkedin.com`, so unrelated URLs like `notlinkedin.com` can be misclassified as LinkedIn. Match the hostname boundary instead of using a raw substring check.</comment>

<file context>
@@ -10,6 +10,7 @@ export function getSocialPlatformByLink(link: string): string {
   if (link.includes("instagram.com")) return "INSTAGRAM";
   if (link.includes("spotify.com")) return "SPOTIFY";
   if (link.includes("tiktok.com")) return "TIKTOK";
+  if (link.includes("linkedin.com")) return "LINKEDIN";
   if (link.includes("apple.com")) return "APPLE";
   if (link.includes("youtube.")) return "YOUTUBE";
</file context>
Suggested change
if (link.includes("linkedin.com")) return "LINKEDIN";
if (/(^|\/\/|\.)linkedin\.com(\/|$)/.test(link)) return "LINKEDIN";

Comment thread lib/apify/scrapeProfileUrl.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/apify/linkedin/startLinkedinProfileScraping.ts (1)

1-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Extract shared Apify start/validation logic. Several profile scrapers repeat the same run.id / defaultDatasetId guard and FAILED/ABORTED handling. A small helper around apifyClient.actor(...).start(...) would keep the modules aligned; leave the platform-specific input shaping in each scraper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/apify/linkedin/startLinkedinProfileScraping.ts` around lines 1 - 45,
Extract the repeated Apify run validation from startLinkedinProfileScraping into
a shared helper used by the other profile scraper modules. Keep the
LinkedIn-specific input shaping in startLinkedinProfileScraping, but move the
common run.id/defaultDatasetId guard and FAILED/ABORTED handling into a reusable
function around apifyClient.actor(...).start(...). Refer to
startLinkedinProfileScraping, apifyClient.actor, and OUTSTANDING_ERROR when
wiring the helper so the scraper modules stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/apify/linkedin/startLinkedinProfileScraping.ts`:
- Around line 33-35: The terminal-status check in startLinkedinProfileScraping’s
.start() path is misplaced because .start() returns before the run completes, so
FAILED and ABORTED will not be seen here. Update this flow by either switching
the LinkedIn run invocation to .call() if this function must wait for
completion, or remove the run.status check from startLinkedinProfileScraping and
move terminal-state handling into the polling/completion path that observes the
finished run.
- Line 26: The Apify actor start call in startLinkedinProfileScraping is missing
an abort/timeout guard, so it can hang indefinitely on the request path. Update
the scrape flow around
apifyClient.actor("harvestapi/linkedin-profile-scraper").start(input) to use an
AbortController or equivalent timeout mechanism, pass the signal into the
request if supported, and ensure the timeout is handled cleanly by surfacing a
controlled error instead of blocking the request.

---

Nitpick comments:
In `@lib/apify/linkedin/startLinkedinProfileScraping.ts`:
- Around line 1-45: Extract the repeated Apify run validation from
startLinkedinProfileScraping into a shared helper used by the other profile
scraper modules. Keep the LinkedIn-specific input shaping in
startLinkedinProfileScraping, but move the common run.id/defaultDatasetId guard
and FAILED/ABORTED handling into a reusable function around
apifyClient.actor(...).start(...). Refer to startLinkedinProfileScraping,
apifyClient.actor, and OUTSTANDING_ERROR when wiring the helper so the scraper
modules stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 250f01b8-f138-4411-81c9-e0f8d385bc3c

📥 Commits

Reviewing files that changed from the base of the PR and between 1bbefab and af9f2ac.

⛔ Files ignored due to path filters (2)
  • lib/apify/__tests__/scrapeProfileUrl.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artists/__tests__/getSocialPlatformByLink.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • lib/apify/linkedin/startLinkedinProfileScraping.ts
  • lib/apify/scrapeProfileUrl.ts
  • lib/artists/getSocialPlatformByLink.ts

const input = { profiles: [targetUrl] };

try {
const run = await apifyClient.actor("harvestapi/linkedin-profile-scraper").start(input);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout on external Apify call.

apifyClient.actor(...).start(input) has no timeout/abort mechanism. If the Apify API hangs, this call could block the request indefinitely, since it appears to run on a request-handling path via scrapeProfileUrl.

🕐 Suggested guard using AbortController
-    const run = await apifyClient.actor("harvestapi/linkedin-profile-scraper").start(input);
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 30_000);
+    let run;
+    try {
+      run = await apifyClient
+        .actor("harvestapi/linkedin-profile-scraper")
+        .start(input, { signal: controller.signal });
+    } finally {
+      clearTimeout(timeout);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/apify/linkedin/startLinkedinProfileScraping.ts` at line 26, The Apify
actor start call in startLinkedinProfileScraping is missing an abort/timeout
guard, so it can hang indefinitely on the request path. Update the scrape flow
around apifyClient.actor("harvestapi/linkedin-profile-scraper").start(input) to
use an AbortController or equivalent timeout mechanism, pass the signal into the
request if supported, and ensure the timeout is handled cleanly by surfacing a
controlled error instead of blocking the request.

Comment on lines +33 to +35
if (run.status === "FAILED" || run.status === "ABORTED") {
throw new Error(OUTSTANDING_ERROR);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Apify JS client actor().start() return a run with terminal status like FAILED or does it only return the initial queued status?

💡 Result:

The Apify JS client's actor.start method starts an actor and immediately returns the run object, which reflects the initial state of the run, such as "READY" or "RUNNING" [1][2][3]. It does not wait for the actor to complete, so it will not return a terminal status like "SUCCEEDED" or "FAILED" at the time of resolution [1][2][3]. If you need to wait for the actor to finish, you should use the actor.call method instead, which polls the run status and returns the run object only after the actor has completed [2][4]. Alternatively, you can use the start method with a waitForFinish option (in some versions/configurations) or manage the wait manually using the returned run ID [5][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file with line numbers.
sed -n '1,220p' lib/apify/linkedin/startLinkedinProfileScraping.ts | cat -n

# Find nearby Apify start/call usage for context.
rg -n "actor\\.(start|call)|run\\.status|FAILED|ABORTED" lib/apify -S

Repository: recoupable/api

Length of output: 3192


Move terminal-status handling out of this start path .start() resolves before the run finishes, so FAILED/ABORTED won’t be observed here. If this step should wait for completion, use .call(); otherwise, handle terminal states in the polling path instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/apify/linkedin/startLinkedinProfileScraping.ts` around lines 33 - 35, The
terminal-status check in startLinkedinProfileScraping’s .start() path is
misplaced because .start() returns before the run completes, so FAILED and
ABORTED will not be seen here. Update this flow by either switching the LinkedIn
run invocation to .call() if this function must wait for completion, or remove
the run.status check from startLinkedinProfileScraping and move terminal-state
handling into the polling/completion path that observes the finished run.

…dles

Two bugs caught verifying against the live harvestapi actor + a real LinkedIn
social (recoupable/chat#1833):
- The actor's input schema takes `urls` ("Profile URLs") / publicIdentifiers /
  queries — not `profiles`. Sending `profiles` would start a run with no targets.
- getUsernameFromProfileUrl returned the LinkedIn path prefix ("in") instead of
  the handle, so a stored username of "in" would make the scraper build
  linkedin.com/in/in. Teach it linkedin.com/{in,company,school}/<handle>; this
  also fixes the username updateArtistSocials stores for LinkedIn adds.

Verified: harvestapi input schema pulled from the actor's latest build
(uZgrDy65OCHs0Chum); RED→GREEN tests for both.

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

Live preview test: the run queried linkedin.com/in/in → 404 "Profile not found" —
the dispatcher passed the DB row's stored username ("in", written by the old
path-prefix helper bug), which the helper fix can't repair for existing rows.
Add a per-entry input selector to PLATFORM_SCRAPERS; LinkedIn takes the full
profile_url (the field the platform was matched on), making stored-username
quality irrelevant for LinkedIn scrapes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd in the module

Per review: don't modify the shared PLATFORM_SCRAPERS config for one platform.
Revert the input-selector change; instead startLinkedinProfileScraping rejects
LinkedIn path prefixes ("in"/"company"/"school") as handles — legacy rows stored
the prefix as username, and scraping linkedin.com/in/in would hit a real, wrong
profile. Correct usernames come from the fixed getUsernameFromProfileUrl going
forward; bad legacy rows now fail loudly instead of scraping the wrong person.

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

Copy link
Copy Markdown
Contributor Author

Preview verification — LinkedIn scrape works end-to-end (and the input-shape caveat was real)

Verified against a real LinkedIn social (social_id c8e25612…, linkedin.com/in/sweetmaneth, added to a staff-owned artist via PATCH /api/artists/{id}). Final preview: https://api-mz0gk7l9l-recoup.vercel.app (head b65c1d69).

Before / after

result
prod (main, no #737): POST /api/socials/{id}/scrape 400 "Unsupported social media platform"
#737 preview: same call 200 {"runId":"TNwgXpQmhvoCHsKg8","datasetId":"NyvFDOM6gw8DEHBJY"}
poll GET /api/apify/runs/TNwgXpQmhvoCHsKg8 SUCCEEDED — real profile: publicIdentifier: sweetmaneth, headline "The dev for onchain music.", Columbus OH, 14,242 connections, full experience/education payload

The PR-body caveat was warranted — 3 real bugs found & fixed during verification

  1. Wrong actor input key (eec086ca): the harvestapi actor's real input schema (latest build uZgrDy65OCHs0Chum) takes urls ("Profile URLs") — not profiles as originally written. Sending profiles would start runs with no targets.
  2. getUsernameFromProfileUrl returned the LinkedIn path prefix (eec086ca): linkedin.com/in/sweetmaneth"in". Now parses /in|company|school/<handle>; fixes the username updateArtistSocials stores for LinkedIn adds.
  3. Legacy bad usernames scraped the wrong profile (b65c1d69): caught live — the first preview run (AB81HuE3atfKvhIKh) queried linkedin.com/in/in404 Profile not found, because the dispatcher passes the stored DB username ("in" on the pre-fix row). Per review (KISS: don't touch the shared PLATFORM_SCRAPERS for one platform), the fix lives in the LinkedIn module only: startLinkedinProfileScraping rejects path-prefix pseudo-handles (in/company/school) with a loud error instead of silently scraping a real-but-wrong profile. The shared scrapeProfileUrl.ts is byte-identical to main + the one dispatch entry.

All RED→GREEN TDD'd; lib/apify + lib/socials suites green (74); tsc + eslint clean.

Notes

  • Data repair: the test social's stored username was "in" (written pre-fix); repaired to sweetmaneth via a one-off SQL update (no API endpoint updates a socials row's username — re-PATCHing the same URL reuses the row untouched). Future LinkedIn adds store the correct handle via fix adding inputSchema #2.
  • Persistence: the scrape result is retrievable via the poll endpoint but won't persist to socials until the webhook handler work (api#738 + per-platform handlers) — same as TikTok/X/YouTube today.
  • The other pre-existing LinkedIn social in the DB (linkedin.com/in/drew-thurlow-3a354311) has a correct-style username and would scrape fine; any legacy row with a prefix-username now fails loudly rather than wrong.

Net: POST /api/socials/{social_id}/scrape on a linkedin.com profile returns a runId and the poll returns real LinkedIn profile data — the item's Done-when, demonstrated live.

@sweetmantech sweetmantech merged commit 3bedb9d into main Jul 1, 2026
6 checks passed
sweetmantech added a commit that referenced this pull request Jul 1, 2026
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