Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions src/pages/ContributorProfile/ContributorProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,96 @@ type PR = {
type Profile = {
avatar_url: string;
login: string;
bio: string;
bio: string | null;
};

const isProfile = (data: unknown): data is Profile => {
return (
typeof data === "object" &&
data !== null &&
typeof (data as any).avatar_url === "string" &&
typeof (data as any).login === "string" &&
(typeof (data as any).bio === "string" || (data as any).bio === null)
);
Comment on lines +17 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify remaining explicit-any usage in this file
rg -n --type=ts --type=tsx '\bas\s+any\b' src/pages/ContributorProfile/ContributorProfile.tsx

Repository: GitMetricsLab/github_tracker

Length of output: 100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/pages/ContributorProfile/ContributorProfile.tsx"

echo "== any/as any occurrences =="
rg -n --no-heading --hidden --follow --glob "$FILE" '\bas\s+any\b|\bno-explicit-any\b|\b:\s*any\b' "$FILE" || true

echo
echo "== context around the previously cited lines (15-45) =="
nl -ba "$FILE" | sed -n '1,120p'

echo
echo "== context around the previously cited lines (70-120) =="
nl -ba "$FILE" | sed -n '70,140p'

Repository: GitMetricsLab/github_tracker

Length of output: 719


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/pages/ContributorProfile/ContributorProfile.tsx"
wc -l "$FILE"

echo
echo "== Lines 1-60 =="
cat -n "$FILE" | sed -n '1,60p'

echo
echo "== Lines 70-120 =="
cat -n "$FILE" | sed -n '70,120p'

Repository: GitMetricsLab/github_tracker

Length of output: 3996


Remove as any casts in runtime guards/PR parsing to satisfy no-explicit-any and keep validation type-safe.

src/pages/ContributorProfile/ContributorProfile.tsx still uses as any at lines 21-23, 34-36, 91-92, and 97. Replace these casts with Record<string, unknown>-based narrowing and a small hasItems guard.

Suggested fix
+type UnknownRecord = Record<string, unknown>;
+
 const isProfile = (data: unknown): data is Profile => {
+  const d = data as UnknownRecord;
   return (
     typeof data === "object" &&
     data !== null &&
-    typeof (data as any).avatar_url === "string" &&
-    typeof (data as any).login === "string" &&
-    (typeof (data as any).bio === "string" || (data as any).bio === null)
+    typeof d.avatar_url === "string" &&
+    typeof d.login === "string" &&
+    (typeof d.bio === "string" || d.bio === null)
   );
 };
 
 const isPrArray = (data: unknown): data is PR[] => {
   return (
     Array.isArray(data) &&
     data.every(
       (item) =>
         typeof item === "object" &&
         item !== null &&
-        typeof (item as any).title === "string" &&
-        typeof (item as any).html_url === "string" &&
-        typeof (item as any).repository_url === "string"
+        typeof (item as UnknownRecord).title === "string" &&
+        typeof (item as UnknownRecord).html_url === "string" &&
+        typeof (item as UnknownRecord).repository_url === "string"
     )
   );
 };
+
+const hasItems = (data: unknown): data is { items: unknown } =>
+  typeof data === "object" && data !== null && "items" in data;
 
-        if (
-          typeof prsData !== "object" ||
-          prsData === null ||
-          !Array.isArray((prsData as any).items) ||
-          !isPrArray((prsData as any).items)
-        ) {
+        if (!hasItems(prsData) || !Array.isArray(prsData.items) || !isPrArray(prsData.items)) {
           throw new Error("Invalid GitHub PR response.");
         }
 
-        setPRs((prsData as any).items);
+        setPRs(prsData.items);
🧰 Tools
🪛 ESLint

[error] 21-21: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 22-22: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 23-23: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 23-23: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 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 `@src/pages/ContributorProfile/ContributorProfile.tsx` around lines 17 - 24,
The runtime type checks in ContributorProfile use `as any`; replace those with
safe narrowing via `Record<string, unknown>` and a small `hasItems` helper: in
`isProfile`, cast `data` to `const obj = data as Record<string, unknown>` and
check properties with `typeof obj.avatar_url === 'string'`, `typeof obj.login
=== 'string'`, and `(typeof obj.bio === 'string' || obj.bio === null)`. Add a
`function hasItems(value: unknown): value is unknown[] { return
Array.isArray(value) && value.length > 0; }` and use it where array casts were
done to validate non-empty arrays and element types instead of `as any`. Apply
the same pattern to the other runtime guards in this file (replace all `as any`
usages) so all runtime parsing is done via `Record<string, unknown>` checks and
`hasItems` assertions.

};
Comment thread
Tanayajadhav1 marked this conversation as resolved.

const isPrArray = (data: unknown): data is PR[] => {
return (
Array.isArray(data) &&
data.every(
(item) =>
typeof item === "object" &&
item !== null &&
typeof (item as any).title === "string" &&
typeof (item as any).html_url === "string" &&
typeof (item as any).repository_url === "string"
)
);
};

export default function ContributorProfile() {
const { username } = useParams();
const [profile, setProfile] = useState<Profile | null>(null);
const [prs, setPRs] = useState<PR[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchData() {
if (!username) return;
if (!username) {
setError("No username provided.");
setLoading(false);
return;
}
Comment thread
Tanayajadhav1 marked this conversation as resolved.

setLoading(true);
setError(null);

try {
const userRes = await fetch(`https://api.github.com/users/${username}`);
Comment thread
Tanayajadhav1 marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and print the relevant section around the cited line(s).
FILE="src/pages/ContributorProfile/ContributorProfile.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "== File: $FILE =="
wc -l "$FILE"
sed -n '1,140p' "$FILE" | nl -ba | sed -n '40,100p'

# Also search for usages of `username` and GitHub URL construction within the file.
echo "== rg: username in ContributorProfile.tsx =="
rg -n "username" "$FILE"

echo "== rg: github.com in ContributorProfile.tsx =="
rg -n "github\.com" "$FILE"

Repository: GitMetricsLab/github_tracker

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/pages/ContributorProfile/ContributorProfile.tsx"

echo "== File: $FILE =="
wc -l "$FILE"

echo "== Lines 40-100 =="
awk 'NR>=40 && NR<=100 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo "== rg: username in ContributorProfile.tsx =="
rg -n "username" "$FILE" || true

echo "== rg: github.com in ContributorProfile.tsx =="
rg -n "github\.com" "$FILE" || true

echo "== Print fetch/search URL constructions =="
rg -n "fetch\\(|search/issues" "$FILE" || true

Repository: GitMetricsLab/github_tracker

Length of output: 3230


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/pages/ContributorProfile/ContributorProfile.tsx"
rg -n "encodeURIComponent" "$FILE" || true

Repository: GitMetricsLab/github_tracker

Length of output: 54


Encode username before building GitHub URLs.
username is interpolated raw into the GitHub user fetch URL and the PR search q parameter; special characters can break requests or change query semantics.

Suggested fix
-        const userRes = await fetch(`https://api.github.com/users/${username}`);
+        const encodedUsername = encodeURIComponent(username);
+        const userRes = await fetch(`https://api.github.com/users/${encodedUsername}`);
...
-          `https://api.github.com/search/issues?q=author:${username}+type:pr`
+          `https://api.github.com/search/issues?q=author:${encodedUsername}+type:pr`
🤖 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 `@src/pages/ContributorProfile/ContributorProfile.tsx` at line 60, The code
interpolates username raw into GitHub URLs (e.g., the fetch call in
ContributorProfile.tsx: const userRes = await
fetch(`https://api.github.com/users/${username}``) and the PR search q
parameter), which can break requests for usernames with special characters; fix
by applying encodeURIComponent(username) wherever username is inserted into URLs
(both the user fetch and the PR search query construction) so the resulting
URL/query is properly escaped while preserving existing logic and variable
names.

if (!userRes.ok) {
if (userRes.status === 404) {
throw new Error("User not found.");
}
if (userRes.status === 403) {
throw new Error("GitHub API rate limit exceeded. Please try again later.");
}
throw new Error(`GitHub user fetch failed with status ${userRes.status}.`);
}

const userData = await userRes.json();
if (!isProfile(userData)) {
throw new Error("Invalid GitHub profile response.");
}
setProfile(userData);

const prsRes = await fetch(
`https://api.github.com/search/issues?q=author:${username}+type:pr`
);
if (!prsRes.ok) {
if (prsRes.status === 403) {
throw new Error("GitHub API rate limit exceeded. Please try again later.");
}
throw new Error(`GitHub PR fetch failed with status ${prsRes.status}.`);
}

const prsData = await prsRes.json();
setPRs(prsData.items);
} catch {
toast.error("Failed to fetch user data.");
if (
typeof prsData !== "object" ||
prsData === null ||
!Array.isArray((prsData as any).items) ||
!isPrArray((prsData as any).items)
) {
throw new Error("Invalid GitHub PR response.");
}

setPRs((prsData as any).items);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to fetch user data.";
setError(message);
toast.error(message);
setProfile(null);
setPRs([]);
} finally {
setLoading(false);
}
Expand All @@ -51,6 +116,11 @@ export default function ContributorProfile() {

if (loading) return <div className="text-center mt-10">Loading...</div>;

if (error)
return (
<div className="text-center mt-10 text-red-600">{error}</div>
);

if (!profile)
return (
<div className="text-center mt-10 text-red-600">User not found.</div>
Expand Down
112 changes: 107 additions & 5 deletions src/pages/Contributors/Contributors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,104 @@ interface Contributor {
html_url: string;
}

interface FetchError {
message: string;
isRateLimited: boolean;
statusCode?: number;
}

// Custom error class for Contributors fetch errors
class ContributorsError extends Error {
constructor(
message: string,
public isRateLimited = false,
public statusCode?: number
) {
super(message);
this.name = "ContributorsError";
}
}
Comment thread
Tanayajadhav1 marked this conversation as resolved.

// Type guard to validate if data is Contributor[]
const isContributorArray = (data: unknown): data is Contributor[] => {
if (!Array.isArray(data)) return false;
return data.every((item) => {
if (typeof item !== "object" || item === null) return false;

// Validate all required fields with correct types
return (
typeof item.id === "number" &&
typeof item.login === "string" &&
typeof item.avatar_url === "string" &&
typeof item.contributions === "number" &&
typeof item.html_url === "string"
);
});
};

const ContributorsPage = () => {
const [contributors, setContributors] = useState<Contributor[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState<FetchError | null>(null);

// Fetch contributors from GitHub API
useEffect(() => {
const fetchContributors = async () => {
try {
setLoading(true);
setError(null);

const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, {
withCredentials: false,
timeout: 10000,
});

// ✅ Validate response structure matches Contributor[]
if (!isContributorArray(response.data)) {
throw new ContributorsError(
"Invalid API response structure. Expected array of contributors.",
false
);
}

setContributors(response.data);
} catch {
setError("Failed to fetch contributors. Please try again later.");
} catch (err) {
const fetchError: FetchError = {
message: "Failed to fetch contributors. Please try again later.",
isRateLimited: false,
};

// Handle ContributorsError instances
if (err instanceof ContributorsError) {
fetchError.message = err.message;
fetchError.isRateLimited = err.isRateLimited;
fetchError.statusCode = err.statusCode;
} else if (axios.isAxiosError(err)) {
// Handle Axios errors
if (err.response?.status === 403) {
fetchError.message =
"GitHub API rate limit exceeded. Try again later.";
fetchError.isRateLimited = true;
fetchError.statusCode = 403;
} else if (err.response?.status === 404) {
fetchError.message = "Repository not found.";
fetchError.statusCode = 404;
} else if (err.code === "ECONNABORTED") {
fetchError.message = "Request timeout. Server took too long to respond.";
fetchError.statusCode = 408;
} else if (err.response?.status) {
fetchError.message = `HTTP ${err.response.status}: Failed to fetch contributors`;
fetchError.statusCode = err.response.status;
} else if (err.message) {
fetchError.message = err.message;
}
} else if (err instanceof Error) {
fetchError.message = err.message || fetchError.message;
}

setError(fetchError);
console.error("Contributors fetch error:", fetchError);
setContributors([]);
} finally {
setLoading(false);
}
Expand All @@ -57,8 +140,27 @@ const ContributorsPage = () => {

if (error) {
return (
<Box sx={{ mt: 4 }}>
<Alert severity="error">{error}</Alert>
<Box sx={{ mt: 4, mx: 2 }}>
<Alert severity="error" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: "bold" }}>
⚠️ {error.message}
</Typography>
Comment thread
Tanayajadhav1 marked this conversation as resolved.
{error.isRateLimited && (
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
You've hit GitHub's API rate limit. The limit resets in 1 hour.
</Typography>
Comment on lines +149 to +151
)}
{error.statusCode === 404 && (
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
Please verify the repository exists and is accessible.
</Typography>
)}
{error.statusCode === 408 && (
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
The server took too long to respond. Please try again.
</Typography>
)}
</Alert>
</Box>
);
}
Expand Down
Loading