RED-104: Real shadowban detection with profile analysis#180
Merged
Conversation
- Fetch profile data + recent activity in parallel from Reddit - Detect: shadowban (no visible activity), suspension, negative karma, not found - Show actual account details: karma, age, email verified, activity count - Return specific result codes: CLEAR, SHADOWBANNED, SUSPENDED, AT_RISK, NOT_FOUND, UNREACHABLE - Updated frontend with color-coded results and account detail cards - Tests cover all result paths (7 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Upgrades the shadowban-check tool from a simple reachability test to a more detailed Reddit profile + recent-activity analysis (via the existing Cloudflare proxy), and updates the public UI and unit tests to match the richer verdicts and payload.
Changes:
- Update API route to fetch
/about.jsonplus recent activity, derive new results (CLEAR/SHADOWBANNED/SUSPENDED/AT_RISK/NOT_FOUND/UNREACHABLE), and return areason+profilesummary. - Update public shadowban-check page to display the new result categories and account details.
- Rewrite unit tests to mock
fetchRedditJsonand cover the new result paths.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/unit/api/shadowbanCheckToolRoute.test.ts | Updates tests to mock the proxy fetch and assert new response/result shapes. |
| src/app/api/tools/shadowban-check/route.ts | Implements profile/activity fetching and new verdict logic; extends response payload. |
| src/app/(public)/tools/shadowban-check/page.tsx | Updates UI to present new verdicts, reason text, and account detail fields. |
Comments suppressed due to low confidence (1)
tests/unit/api/shadowbanCheckToolRoute.test.ts:75
beforeEachusesjest.clearAllMocks(), which does not reset mock implementations. Since tests setmockImplementation/mockRejectedValueonfetchRedditJson, the implementation can leak across tests and make ordering matter. Preferjest.resetAllMocks()here or explicitlymockedProxy.fetchRedditJson.mockReset()inbeforeEachbefore re-stubbing.
beforeEach(() => {
jest.clearAllMocks();
mockedGuards.requireSession.mockRejectedValue(new Error("UNAUTHORIZED"));
mockedRateLimit.enforcePublicToolRateLimit.mockResolvedValue({
allowed: true,
limit: 20,
remaining: 19,
resetAfterSeconds: 60,
});
mockedPrisma.visibilityCheck.findMany.mockResolvedValue([]);
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Comment on lines
+34
to
+41
| const RESULT_DISPLAY: Record<string, { label: string; color: string }> = { | ||
| CLEAR: { label: "Account looks healthy", color: "text-emerald-600" }, | ||
| SHADOWBANNED: { label: "Likely shadowbanned", color: "text-red-600" }, | ||
| SUSPENDED: { label: "Account suspended", color: "text-red-600" }, | ||
| AT_RISK: { label: "At risk", color: "text-amber-600" }, | ||
| NOT_FOUND: { label: "Account not found", color: "text-muted-foreground" }, | ||
| UNREACHABLE: { label: "Check failed", color: "text-muted-foreground" }, | ||
| }; |
Comment on lines
+125
to
+147
| const [profileRes, activityRes] = await Promise.all([ | ||
| fetchRedditJson(`/user/${encodeURIComponent(username)}/about.json`, { | ||
| signal: controller.signal, | ||
| }), | ||
| fetchRedditJson(`/user/${encodeURIComponent(username)}/.json`, { | ||
| signal: controller.signal, | ||
| }).catch(() => null), | ||
| ]); | ||
|
|
||
| profileStatus = profileRes.status; | ||
| profileOk = profileRes.ok; | ||
|
|
||
| if (profileOk) { | ||
| const body = (await profileRes.json()) as { data?: ProfileData }; | ||
| profileData = body?.data ?? null; | ||
| } | ||
|
|
||
| if (activityRes?.ok) { | ||
| const body = (await activityRes.json()) as { | ||
| data?: { children?: unknown[] }; | ||
| }; | ||
| activityCount = body?.data?.children?.length ?? 0; | ||
| } |
Comment on lines
+64
to
+69
| if (s.internalRisk > 0.5) | ||
| return { | ||
| result: "AT_RISK", | ||
| reason: | ||
| "Internal signals indicate elevated visibility risk from past checks.", | ||
| }; |
- Remove cross-workspace data leakage (internal signals no longer exposed) - Fix username validation: allow u/ prefix (max 22 chars), strip before validate - Separate JSON parse try/catch to avoid overwriting profileStatus - Treat 403 as shadowban signal (not just unreachable) - Remove Prisma dependency (public tool no longer queries internal data) - Add tests: rate limiting, malformed JSON, u/ prefix, 403 shadowban, no leakage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Upgrade shadowban checker from basic reachability check to real profile analysis.
Before: Only checked if profile URL returns 200/403. Always showed "Unreachable" on Vercel.
After: Fetches profile data + recent activity via Cloudflare proxy. Returns specific verdicts:
Frontend
Shows actual account details: karma, account age, email verified, activity count, suspension status. Color-coded results.
Test Plan
🤖 Generated with Claude Code