Fetch profile avatars via GraphQL profile query - #3
Conversation
📝 WalkthroughInstagram Avatar Fetching - Now Using GraphQL (PR
|
| Cohort / File(s) | Summary |
|---|---|
Test Updates src/content/profile.test.ts |
Avatar resolution tests refactored to model two-step network flow: fetch profile page HTML to extract user ID, then fetch profile data via GraphQL. Assertions updated for new fetch ordering, URLs, request headers (credentials, X-IG-App-ID), and cache validation for profile_user_id_by_username. |
Core Implementation src/content/profile.ts |
Introduces username-normalized user ID caching and multi-step resolution pipeline with fallbacks (web profile, HTML scrape, GraphQL query, search endpoints). Adds helpers for cookie-bearing requests, CSRF token reading, and recursive response object traversal. Extends avatar resolution with GraphQL queries and refactors avatar fetching into shared fetchProfileAvatarData helper. |
Sequence Diagram(s)
sequenceDiagram
participant Client as Client/Extension
participant Cache as Local Storage
participant IGWeb as Instagram Web
participant GraphQL as GraphQL Endpoint
participant Search as Search API
Client->>Cache: Check if user_id cached?
alt User ID cached
Cache-->>Client: Return user_id
else User ID not cached
Client->>IGWeb: Fetch profile page HTML<br/>(https://instagram.com/username/)
IGWeb-->>Client: HTML + user_id in response
alt user_id extracted
Client->>Cache: Store user_id by username
Cache-->>Client: ✓ Cached
else Extraction fails
Client->>GraphQL: Query via GraphQL doc_id
GraphQL-->>Client: Profile data with user_id
alt Still no user_id
Client->>Search: Try search endpoints<br/>(top search, Instagram search)
Search-->>Client: user_id from search results
end
Client->>Cache: Store resolved user_id
end
end
Client->>Cache: Check avatar cache?
alt Avatar cached
Cache-->>Client: Return cached avatar
else Avatar not cached
Client->>GraphQL: Fetch avatar variants<br/>(GraphQL query with user_id)
GraphQL-->>Client: HD avatar URL
Client->>Cache: Store avatar URL
Cache-->>Client: ✓ Cached
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
The implementation introduces significant new logic across multiple fallback pathways, adds several new helper functions for cookie handling and recursive object traversal, integrates GraphQL queries alongside existing REST endpoints, and requires careful validation of the caching layer and error handling flow.
Poem
🎭 A user ID quest through fallback's delight,
GraphQL queries dancing in the night,
Cache remembers what once was sought,
Avatar resolution: a lesson in robustness taught! 📸
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | The title clearly summarizes the main change: fetching profile avatars via GraphQL instead of other methods, which matches the primary objective of the PR. |
| Description check | ✅ Passed | The description is directly related to the changeset, outlining the three key changes: user ID resolution, GraphQL-based avatar fetching, and fallback routes, with testing details. |
| 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. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
📝 Generate docstrings
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
codex/avatar-graphql-profile-query-clean
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.
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/content/profile.ts (1)
340-347: ⚡ Quick winLet this fallback inspect more than one TopSearch hit.
Line 342 hard-codes
count=1, butfindUserIdByUsername()is already written to walk a result list and find an exact username match. If the real account is ranked second or third, this fallback fails even though the endpoint returned enough data to succeed. Asking for a small batch like 10 keeps the logic the same and makes this fallback actually useful.🔧 Small tweak
async function getUserIdFromTopSearch(username: string) { const data = await fetchInstagramJson( - `https://www.instagram.com/web/search/topsearch/?context=blended&query=${encodeURIComponent(username)}&rank_token=0.3953592318270893&count=1` + `https://www.instagram.com/web/search/topsearch/?context=blended&query=${encodeURIComponent(username)}&rank_token=0.3953592318270893&count=10` ); const userId = findUserIdByUsername(data, username);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/content/profile.ts` around lines 340 - 347, getUserIdFromTopSearch currently requests only one TopSearch hit (count=1) so findUserIdByUsername can't inspect lower-ranked matches; update the query string passed to fetchInstagramJson in getUserIdFromTopSearch to request a small batch (e.g., count=10) so findUserIdByUsername can scan multiple results and find exact username matches; leave the rest of the function logic (fetchInstagramJson call and subsequent findUserIdByUsername usage and error handling) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/content/profile.ts`:
- Around line 390-418: fetchProfileAvatarUrlFromGraphQL currently lets a low-res
GraphQL URL short-circuit fetchProfileAvatarUrl; change the flow so
fetchProfileAvatarUrlFromGraphQL returns an object/structure (or two values)
indicating highResolutionUrl vs lowResolutionCandidate (use
getHighResolutionProfileAvatarUrlFromApiData and
getProfileAvatarUrlFromApiData), and update fetchProfileAvatarUrl to only return
early when a confirmed HD URL is found; if only a low-res candidate is returned,
keep it as a fallback candidate (do not cache/return it yet) and continue trying
the legacy endpoints (/api/v1/users/web_profile_info,
/api/v1/feed/user/.../username/, /api/v1/users/{id}/info) to look for
profile_pic_url_hd or hd_profile_pic_url_info, then choose the best available
URL and call cacheProfileAvatarUrl once for the final chosen URL; add a
regression test that simulates GraphQL returning only low-res and a legacy
endpoint returning HD to ensure legacy HD wins.
---
Nitpick comments:
In `@src/content/profile.ts`:
- Around line 340-347: getUserIdFromTopSearch currently requests only one
TopSearch hit (count=1) so findUserIdByUsername can't inspect lower-ranked
matches; update the query string passed to fetchInstagramJson in
getUserIdFromTopSearch to request a small batch (e.g., count=10) so
findUserIdByUsername can scan multiple results and find exact username matches;
leave the rest of the function logic (fetchInstagramJson call and subsequent
findUserIdByUsername usage and error handling) unchanged.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c8de9525-8331-460a-aa50-39fd41556dec
📒 Files selected for processing (2)
src/content/profile.test.tssrc/content/profile.ts
📜 Review details
🔇 Additional comments (1)
src/content/profile.test.ts (1)
202-306: Nice coverage for the new two-step avatar flow.These tests lock down the important behavior that changed here: the profile-page fetch happens first, the GraphQL request happens second,
credentials: 'include'is preserved,X-IG-App-IDis sent, and the newprofile_user_id_by_usernamecache is written. That gives this refactor a pretty solid safety net.
| async function fetchProfileAvatarUrlFromGraphQL(username: string) { | ||
| const userId = await getUserId(username); | ||
| const variables = encodeURIComponent(JSON.stringify({ | ||
| id: userId, | ||
| render_surface: 'PROFILE', | ||
| })); | ||
| const generatedUrl = `https://www.instagram.com/graphql/query/?doc_id=${PROFILE_AVATAR_GRAPHQL_DOC_ID}&variables=${variables}`; | ||
| const data = await fetchProfileAvatarData(generatedUrl); | ||
| const highResolutionUrl = getHighResolutionProfileAvatarUrlFromApiData(data); | ||
| const fallbackUrl = getProfileAvatarUrlFromApiData(data); | ||
|
|
||
| if (highResolutionUrl) { | ||
| await cacheProfileAvatarUrl(username, highResolutionUrl); | ||
| return highResolutionUrl; | ||
| } | ||
|
|
||
| if (fallbackUrl) { | ||
| await cacheProfileAvatarUrl(username, fallbackUrl, 'fallback'); | ||
| return fallbackUrl; | ||
| } | ||
| } | ||
|
|
||
| async function fetchProfileAvatarUrl(username: string) { | ||
| const appId = findAppId() || '936619743392459'; | ||
| try { | ||
| const graphQLUrl = await fetchProfileAvatarUrlFromGraphQL(username); | ||
| if (graphQLUrl) return graphQLUrl; | ||
| } catch (error) { | ||
| console.log(`Failed to fetch profile avatar from GraphQL profile query: ${error}`); | ||
| } |
There was a problem hiding this comment.
Don’t let a low-res GraphQL result skip the older HD fallback routes.
Right now fetchProfileAvatarUrlFromGraphQL() returns fallbackUrl when GraphQL only gives you profile_pic_url, and fetchProfileAvatarUrl() treats any returned value as success. So a low-resolution GraphQL avatar stops the flow before /api/v1/users/web_profile_info/, /api/v1/feed/user/.../username/, or /api/v1/users/{id}/info/ get a chance to return profile_pic_url_hd or hd_profile_pic_url_info. That’s a real behavior change from “GraphQL first, older routes as fallbacks” to “GraphQL wins even when it’s only low-res.”
A safer pattern is: return early only for a confirmed HD GraphQL hit, otherwise keep the low-res GraphQL URL as a candidate and continue through the legacy endpoints. I’d also add a regression test for “GraphQL returns only low-res, REST fallback returns HD”.
🔧 Possible shape for the fix
-async function fetchProfileAvatarUrlFromGraphQL(username: string) {
+async function fetchProfileAvatarUrlFromGraphQL(username: string) {
const userId = await getUserId(username);
const variables = encodeURIComponent(JSON.stringify({
id: userId,
render_surface: 'PROFILE',
}));
const generatedUrl = `https://www.instagram.com/graphql/query/?doc_id=${PROFILE_AVATAR_GRAPHQL_DOC_ID}&variables=${variables}`;
const data = await fetchProfileAvatarData(generatedUrl);
const highResolutionUrl = getHighResolutionProfileAvatarUrlFromApiData(data);
const fallbackUrl = getProfileAvatarUrlFromApiData(data);
if (highResolutionUrl) {
await cacheProfileAvatarUrl(username, highResolutionUrl);
- return highResolutionUrl;
+ return { quality: 'high' as const, url: highResolutionUrl };
}
- if (fallbackUrl) {
- await cacheProfileAvatarUrl(username, fallbackUrl, 'fallback');
- return fallbackUrl;
- }
+ return fallbackUrl
+ ? { quality: 'fallback' as const, url: fallbackUrl }
+ : undefined;
}
async function fetchProfileAvatarUrl(username: string) {
+ let fallbackUrl: string | undefined;
+
try {
- const graphQLUrl = await fetchProfileAvatarUrlFromGraphQL(username);
- if (graphQLUrl) return graphQLUrl;
+ const graphQLAvatar = await fetchProfileAvatarUrlFromGraphQL(username);
+ if (graphQLAvatar?.quality === 'high') return graphQLAvatar.url;
+ fallbackUrl = graphQLAvatar?.url;
} catch (error) {
console.log(`Failed to fetch profile avatar from GraphQL profile query: ${error}`);
}
const endpoints = [
`https://www.instagram.com/api/v1/users/web_profile_info/?username=${encodeURIComponent(username)}`,
`https://www.instagram.com/api/v1/feed/user/${encodeURIComponent(username)}/username/`,
];
const seenEndpoints = new Set<string>();
- let fallbackUrl: string | undefined;
for (let i = 0; i < endpoints.length; i++) {
const endpoint = endpoints[i];
...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/content/profile.ts` around lines 390 - 418,
fetchProfileAvatarUrlFromGraphQL currently lets a low-res GraphQL URL
short-circuit fetchProfileAvatarUrl; change the flow so
fetchProfileAvatarUrlFromGraphQL returns an object/structure (or two values)
indicating highResolutionUrl vs lowResolutionCandidate (use
getHighResolutionProfileAvatarUrlFromApiData and
getProfileAvatarUrlFromApiData), and update fetchProfileAvatarUrl to only return
early when a confirmed HD URL is found; if only a low-res candidate is returned,
keep it as a fallback candidate (do not cache/return it yet) and continue trying
the legacy endpoints (/api/v1/users/web_profile_info,
/api/v1/feed/user/.../username/, /api/v1/users/{id}/info) to look for
profile_pic_url_hd or hd_profile_pic_url_info, then choose the best available
URL and call cacheProfileAvatarUrl once for the final chosen URL; add a
regression test that simulates GraphQL returning only low-res and a legacy
endpoint returning HD to ensure legacy HD wins.
Summary
9539110062771438Tests
pnpm exec tsc --noEmitpnpm exec vitest run src/content/profile.test.tspnpm test