Skip to content

fix(socials): canonicalize twitter.com → x.com in normalizeProfileUrl#755

Merged
sweetmantech merged 1 commit into
mainfrom
fix/x-domain-canonicalization
Jul 6, 2026
Merged

fix(socials): canonicalize twitter.com → x.com in normalizeProfileUrl#755
sweetmantech merged 1 commit into
mainfrom
fix/x-domain-canonicalization

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Canonicalizes twitter.com/*x.com/* inside normalizeProfileUrl so every reader/writer agrees on one socials.profile_url upsert key. Fixes the X scrape gap where results persisted to orphaned x.com/* twin rows while artists stayed connected to stale twitter.com/* rows.

Tracking issue: recoupable/chat#1851 (PR #1 of 2 required + 1 optional). Canonical-domain decision (x.com — official brand, matches the Apify actor's echo) recorded on the issue 2026-07-06.

What changed

  • lib/socials/normalizeProfileUrl.ts: after stripping protocol/www./trailing slash, rewrite a leading twitter.com host to x.com (anchored — nottwitter.com and path occurrences untouched; case-insensitive host match; preserves handle casing).
  • lib/socials/__tests__/normalizeProfileUrl.test.ts: 5 new canonicalization cases (equal keys for both spellings, x.com passthrough, no-false-positive domains, bare domain); 2 existing expectations updated to the new contract.

Merge sequencing

No docs PR — no request/response contract change (same reasoning as docs#262 closed unmerged on chat#1840). This PR leads; the database dedupe migration (chat#1851 PR #2) follows it, since the migration implements the same canonical-domain decision. Independent of the optional tweetless-fallback PR.

Tests

  • RED→GREEN: new cases failed against the old implementation (5 failed | 9 passed), pass after (14 passed).
  • Full domain sweep: vitest run lib/socials lib/apify lib/artist lib/artists53 files, 263 tests passed.
  • tsc --noEmit: 200 pre-existing errors on clean origin/test (all in lib/trigger tests), 200 after — no new errors. eslint clean on changed files.

Done-when (from chat#1851)

  • Unit test normalizeProfileUrl("https://x.com/Foo") === normalizeProfileUrl("https://twitter.com/Foo")
  • Live X scrape for an artist connected to a twitter.com/* row updates that row (preview verification pending — note: full effect on existing rows also needs the PR adding inputSchema #2 migration, since current connected rows keep their twitter.com keys until migrated)

🤖 Generated with Claude Code


Summary by cubic

Canonicalizes twitter.com/* to x.com/* during profile URL normalization so all readers/writers use the same upsert key. Fixes X scrape write-backs landing on orphaned x.com rows while artists stayed linked to twitter.com (chat#1851).

  • Bug Fixes
    • After stripping protocol/www/trailing slash, rewrite leading twitter.com host to x.com (anchored, case-insensitive, preserves handle casing).
    • Tests cover canonicalization, equal keys for both spellings, x.com passthrough, no false positives, and bare domain; updated prior expectations accordingly.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Profile URLs are now normalized more consistently, including converting Twitter/X profile links to the preferred domain format.
    • Existing cleanup still removes protocol, www., and trailing slashes so links are stored in a uniform way.
  • Documentation

    • Expanded guidance for how profile URL normalization handles domain aliases.

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

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

Request Review

@coderabbitai

coderabbitai Bot commented Jul 6, 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: 22 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: 6490641e-2252-41ae-ac9e-02b7b31e7570

📥 Commits

Reviewing files that changed from the base of the PR and between 8521abf and d08139a.

⛔ Files ignored due to path filters (1)
  • lib/socials/__tests__/normalizeProfileUrl.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/socials/normalizeProfileUrl.ts
📝 Walkthrough

Walkthrough

The normalizeProfileUrl function is updated to canonicalize twitter.com profile URLs to x.com after stripping protocol, www. prefix, and trailing slash. JSDoc is expanded to document this domain-alias behavior.

Changes

Profile URL Normalization

Layer / File(s) Summary
Domain aliasing and docs
lib/socials/normalizeProfileUrl.ts
Function now stores a stripped intermediate value and replaces twitter.com (case-insensitive) with x.com, preserving trailing slash or end-of-string; JSDoc updated to describe this canonicalization.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Possibly related issues

Poem

A rabbit hopped through URLs so neat,
Turned old twitter.com into x.com's beat,
Stripped the www, trimmed the slash with grace,
One tidy function, one canonical place. 🐰✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 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.
Solid & Clean Code ✅ Passed normalizeProfileUrl.ts exports one short, cohesive function matching the file name; tests are focused and add no extra responsibilities.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/x-domain-canonicalization

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.

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

🧹 Nitpick comments (1)
lib/socials/normalizeProfileUrl.ts (1)

13-21: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid, well-anchored implementation.

The anchored ^twitter\.com(\/|$) pattern correctly avoids false positives (e.g. nottwitter.com, twitter.com.evil.com) while preserving handle casing. Good use of the capture group to retain the trailing slash or end-of-string boundary.

One gap: the www. (line 18) and protocol (line 17) stripping remain case-sensitive, so a host like WWW.TWITTER.COM/handle would skip both strips entirely and bypass the new alias rewrite too — since stripped would still start with WWW. rather than twitter.com. The scrape write-back path (handleTwitterProfileScraperResults.ts) masks this by calling .toLowerCase() after normalizeProfileUrl, but updateArtistSocials.ts does not, so mixed-case artist-submitted URLs could still land on a different key than expected — which is one of the two paths this PR states it fixes.

💡 Optional fix: make protocol/www stripping case-insensitive
   const stripped = url
-    .replace(/^https?:\/\//, "")
-    .replace(/^www\./, "")
+    .replace(/^https?:\/\//i, "")
+    .replace(/^www\./i, "")
     .replace(/\/$/, "");
🤖 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/socials/normalizeProfileUrl.ts` around lines 13 - 21, The
normalizeProfileUrl helper still misses mixed-case inputs because the protocol
and www stripping are case-sensitive, which can prevent the twitter.com to x.com
rewrite from running. Update normalizeProfileUrl to handle case-insensitive
protocol/www prefixes before the existing anchored twitter.com replacement, so
URLs like WWW.TWITTER.COM/handle normalize consistently for both
updateArtistSocials and handleTwitterProfileScraperResults.
🤖 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.

Nitpick comments:
In `@lib/socials/normalizeProfileUrl.ts`:
- Around line 13-21: The normalizeProfileUrl helper still misses mixed-case
inputs because the protocol and www stripping are case-sensitive, which can
prevent the twitter.com to x.com rewrite from running. Update
normalizeProfileUrl to handle case-insensitive protocol/www prefixes before the
existing anchored twitter.com replacement, so URLs like WWW.TWITTER.COM/handle
normalize consistently for both updateArtistSocials and
handleTwitterProfileScraperResults.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0d27d5b4-5664-4509-9bed-3ab10a072c0d

📥 Commits

Reviewing files that changed from the base of the PR and between 245e221 and 8521abf.

⛔ Files ignored due to path filters (1)
  • lib/socials/__tests__/normalizeProfileUrl.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/socials/normalizeProfileUrl.ts

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

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant Scraper as Apify X Scraper
    participant API as Artist Update API
    participant Normalize as normalizeProfileUrl()
    participant DB as Socials Table
    participant Artist as Artist Entity

    Note over Scraper,Artist: NEW: Canonical domain normalization ensures one upsert key

    Scraper->>API: POST /artist/update (profile_url="https://x.com/ashnikko")
    API->>Normalize: url = "https://x.com/ashnikko"
    Normalize->>Normalize: strip protocol, www., trailing slash
    Normalize->>Normalize: no twitter.com match → leave unchanged
    Normalize-->>API: "x.com/ashnikko"
    API->>DB: upsert socials WHERE profile_url = "x.com/ashnikko"
    DB-->>API: row created/updated
    API->>Artist: connect artist to socials.id

    Note over Scraper,Artist: Any previous twitter.com links also map here

    API->>Normalize: url="https://twitter.com/disclosure"
    Normalize->>Normalize: strip protocol, www., trailing slash
    Normalize->>Normalize: match "twitter.com/" case-insensitive → rewrite to "x.com/"
    Normalize-->>API: "x.com/disclosure"
    API->>DB: upsert socials WHERE profile_url = "x.com/disclosure"
    DB-->>API: same row as x.com spelling (single canonical key)

    alt Old behavior (before this PR)
        Note over Normalize: Different keys for twitter.com vs x.com → twin rows
    else New behavior (after this PR)
        Note over Normalize,DB: One key → scrape updates existing artist links
    end
Loading

Requires human review: Functional change to URL normalization that alters database upsert keys (twitter.com to x.com) – low risk but touches core data logic, better for human review.

Re-trigger cubic

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Preview verification — 2026-07-06

Preview: https://api-fi5cz1p6g-recoup.vercel.app — confirmed built from PR head 8521abfd (GitHub deployment 5332837466, status success, sha match).

Method: created a throwaway artist under the test account, exercised PATCH /api/artists/{id} (the user-facing path that shares normalizeProfileUrl with the scrape write-back), cross-checked the socials table directly, then cleaned up (artist deleted via API HTTP 200, 2 fixture social rows removed).

Check Documented/expected Actual (live) Result
Auth sanity GET /api/accounts/id 200 + accountId HTTP 200, fb678396-…
PATCH profileUrls.TWITTER: https://twitter.com/xcanon_test_1851 stored key is canonical x.com/… response account_socialsx.com/xcanon_test_1851 the fix, live
PATCH same handle spelled https://x.com/xcanon_test_1851 no twin row minted still exactly one social: ['x.com/xcanon_test_1851']
Control: profileUrls.INSTAGRAM (www. + trailing slash) non-X domains untouched instagram.com/xcanon_test_1851 (protocol/www./slash stripped, domain preserved)
No auth 401 HTTP 401
DB source-of-truth: select … from socials where profile_url like '%xcanon_test_1851%' 1 X row keyed x.com/…, zero twitter.com/… rows exactly x.com/xcanon_test_1851 + the instagram control row

Done-when status (chat#1851):

  • normalizeProfileUrl("https://x.com/Foo") === normalizeProfileUrl("https://twitter.com/Foo") — unit test in this PR
  • Live write through the shared path lands on the canonical x.com key (this table). The remaining half of the issue's Done-when — Patrick's three existing artist-connected twitter.com/* rows showing scrape data — is carried by the database#40 migration, since existing rows keep their old keys until migrated.

No secrets echoed; test fixtures fully cleaned up. Merge order per the issue: this PR → database#40.

@sweetmantech sweetmantech changed the base branch from test to main July 6, 2026 19:25
X scrape write-backs land on x.com/* keys (the Apify actor echoes x.com)
while artists stay connected to twitter.com/* rows, so follower counts
never reach the connected social (recoupable/chat#1851). Canonicalizing
in normalizeProfileUrl fixes both the scrape write-back and the artist
profileUrls update path, which share this function for lookup + insert.

Canonical domain decision (x.com) recorded on the issue, 2026-07-06.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sweetmantech sweetmantech force-pushed the fix/x-domain-canonicalization branch from 8521abf to d08139a Compare July 6, 2026 19:25
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sweetmantech sweetmantech merged commit ded19d4 into main Jul 6, 2026
5 checks passed
sweetmantech added a commit to recoupable/database that referenced this pull request Jul 6, 2026
…win rows (#40)

Companion to recoupable/api#755 (normalizeProfileUrl canonicalization).
Merges 403 twitter/x twin pairs (repointing 7 FK columns with unique-
constraint guards) and renames 23,622 twinless twitter.com keys in place.
Post-condition check raises if any twitter.com key survives.

Tracking: recoupable/chat#1851 (PR #2 of 2 required).

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