diff --git a/web/astro.config.mjs b/web/astro.config.mjs index af13b5b..471a2bf 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -4,6 +4,7 @@ import { defineConfig } from 'astro/config'; import svelte from '@astrojs/svelte'; import partytown from '@astrojs/partytown'; import sitemap from '@astrojs/sitemap'; +import { printSummary } from './src/utils/logger.ts'; // Adapters import vercelAdapter from '@astrojs/vercel/serverless'; @@ -21,7 +22,14 @@ const output = import.meta.env.OUTPUT || 'hybrid'; const site = import.meta.env.SITE_URL || 'https://awesome-privacy.xyz'; // Initialize Astro integrations -const integrations = [svelte(), partytown(), sitemap()]; +const buildLogger = { + name: 'build-logger', + hooks: { + 'astro:build:done': () => printSummary(), + }, +}; + +const integrations = [svelte(), partytown(), sitemap(), buildLogger]; // Set the appropriate adapter, based on the deploy target const adapter = { diff --git a/web/src/components/things/ItemGitHubMetrics.astro b/web/src/components/things/ItemGitHubMetrics.astro index ebb7ad4..b21d966 100644 --- a/web/src/components/things/ItemGitHubMetrics.astro +++ b/web/src/components/things/ItemGitHubMetrics.astro @@ -1,5 +1,6 @@ --- import FontAwesome from "@components/form/FontAwesome.svelte"; +import { error } from "@utils/logger"; const { github } = Astro.props; interface GitHubRepoData { @@ -29,7 +30,7 @@ const fetchGitHubData = async (repo: string) => { const response = await fetch(`https://api.github.com/repos/${repo}`, { headers: headers, }).catch((e) => { - console.error(`Network error: ${e.message}`); + error('GitHub API', `Network error for ${repo}: ${e.message}`); // Return a placeholder response to handle this gracefully return null; }); @@ -45,7 +46,7 @@ const fetchGitHubData = async (repo: string) => { if (response.ok) { data = await response.json(); } else { - console.error(`HTTP error: Received status code ${response.status}`); + error('GitHub API', `HTTP ${response.status} for ${repo}`); } return { data, statusCode }; diff --git a/web/src/utils/fetch-android-info.ts b/web/src/utils/fetch-android-info.ts index 8b06d33..3e1b0a6 100644 --- a/web/src/utils/fetch-android-info.ts +++ b/web/src/utils/fetch-android-info.ts @@ -1,3 +1,5 @@ +import { error } from './logger'; + const doubleCheckPackageName = (packageStr: string) => { return packageStr.includes('id=') ? packageStr.split('id=')[1] : packageStr; }; @@ -7,9 +9,14 @@ export const fetchAndroidInfo = async ( ): Promise => { const endpoint = `https://android-app-privacy.as93.net/${doubleCheckPackageName(androidPackage)}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching android data:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('Android', `HTTP ${res.status} for ${androidPackage} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('Android', `Network error for ${androidPackage}: ${err}`); return null; } }; diff --git a/web/src/utils/fetch-data.ts b/web/src/utils/fetch-data.ts index 6ef3cd4..948acb6 100644 --- a/web/src/utils/fetch-data.ts +++ b/web/src/utils/fetch-data.ts @@ -1,4 +1,5 @@ import yaml from 'js-yaml'; +import { error } from './logger'; import type { AwesomePrivacy } from '../types/Service'; @@ -6,10 +7,18 @@ const awesomePrivacyData = 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/awesome-privacy.yml'; export const fetchData = async (): Promise => { - return (await fetch(awesomePrivacyData) - .then((res) => res.text()) - .then((data) => yaml.load(data)) - .catch((err) => console.error('ah crap', err))) as AwesomePrivacy; + try { + const res = await fetch(awesomePrivacyData); + if (!res.ok) { + error('Data', `HTTP ${res.status} fetching awesome-privacy.yml (${awesomePrivacyData})`); + return {} as AwesomePrivacy; + } + const text = await res.text(); + return yaml.load(text) as AwesomePrivacy; + } catch (err) { + error('Data', `Failed to fetch awesome-privacy.yml: ${err}`); + return {} as AwesomePrivacy; + } }; export const slugify = (title: string) => { diff --git a/web/src/utils/fetch-discord-info.ts b/web/src/utils/fetch-discord-info.ts index cfce3ba..33df917 100644 --- a/web/src/utils/fetch-discord-info.ts +++ b/web/src/utils/fetch-discord-info.ts @@ -1,11 +1,18 @@ +import { error } from './logger'; + export const fetchDiscordInfo = async ( discordInvite: string, ): Promise => { const endpoint = `https://discord-invite-info.as93.net/${discordInvite}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching discord data:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('Discord', `HTTP ${res.status} for ${discordInvite} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('Discord', `Network error for ${discordInvite}: ${err}`); return null; } }; diff --git a/web/src/utils/fetch-docker-instructions.ts b/web/src/utils/fetch-docker-instructions.ts index bee7688..c3a0005 100644 --- a/web/src/utils/fetch-docker-instructions.ts +++ b/web/src/utils/fetch-docker-instructions.ts @@ -1,11 +1,18 @@ +import { error } from './logger'; + export const fetchDockerData = async ( serviceName: string, ): Promise => { const endpoint = `https://docker-info.as93.workers.dev/${serviceName}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching docker data:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('Docker', `HTTP ${res.status} for ${serviceName} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('Docker', `Network error for ${serviceName}: ${err}`); return null; } }; diff --git a/web/src/utils/fetch-ios-info.ts b/web/src/utils/fetch-ios-info.ts index fa445ab..8f67c63 100644 --- a/web/src/utils/fetch-ios-info.ts +++ b/web/src/utils/fetch-ios-info.ts @@ -1,11 +1,18 @@ +import { error } from './logger'; + export const fetchIosInfo = async ( iosUrl: string, ): Promise => { const endpoint = `https://ios-app-info.as93.net?appStoreUrl=${iosUrl}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching ios info:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('iOS', `HTTP ${res.status} for ${iosUrl} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('iOS', `Network error for ${iosUrl}: ${err}`); return null; } }; diff --git a/web/src/utils/fetch-privacy-policy.ts b/web/src/utils/fetch-privacy-policy.ts index bb8db11..74ef4a9 100644 --- a/web/src/utils/fetch-privacy-policy.ts +++ b/web/src/utils/fetch-privacy-policy.ts @@ -1,11 +1,18 @@ +import { error } from './logger'; + export const fetchTosdrPrivacy = async ( serviceId: string, ): Promise => { const endpoint = `https://privacy-policies.as93.workers.dev/${serviceId}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching privacy policy data:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('ToS;DR', `HTTP ${res.status} for service ${serviceId} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('ToS;DR', `Network error for service ${serviceId}: ${err}`); return null; } }; diff --git a/web/src/utils/fetch-reddit-info.ts b/web/src/utils/fetch-reddit-info.ts index 22e18d2..54c89cf 100644 --- a/web/src/utils/fetch-reddit-info.ts +++ b/web/src/utils/fetch-reddit-info.ts @@ -1,11 +1,18 @@ +import { error } from './logger'; + export const fetchRedditInfo = async ( subreddit: string, ): Promise => { const endpoint = `https://subreddit-info.as93.net/${subreddit}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching reddit data:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('Reddit', `HTTP ${res.status} for r/${subreddit} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('Reddit', `Network error for r/${subreddit}: ${err}`); return null; } }; diff --git a/web/src/utils/fetch-repo-info.ts b/web/src/utils/fetch-repo-info.ts index b863a00..d783040 100644 --- a/web/src/utils/fetch-repo-info.ts +++ b/web/src/utils/fetch-repo-info.ts @@ -1,17 +1,107 @@ +import { error } from './logger'; + +const githubHeaders = (): Record => { + const headers: Record = { + 'User-Agent': 'awesome-privacy', + 'Accept': 'application/vnd.github.v3+json', + }; + const token = import.meta.env.GITHUB_API_KEY; + if (token) headers['Authorization'] = `token ${token}`; + return headers; +}; + +const fetchFromGitHub = async (github: string): Promise => { + const base = `https://api.github.com/repos/${github}`; + const headers = githubHeaders(); + + const [infoRes, langsRes, tagsRes, contribRes, commitsRes] = await Promise.all([ + fetch(base, { headers }), + fetch(`${base}/languages`, { headers }), + fetch(`${base}/tags`, { headers }), + fetch(`${base}/contributors`, { headers }), + fetch(`${base}/commits`, { headers }), + ]); + + if (!infoRes.ok) { + error('GitHub Stats', `GitHub API returned ${infoRes.status} for ${github}`); + return null; + } + + const parseJson = (r: Response) => + r.ok && r.status !== 204 ? r.json() : null; + + const [info, languages, tags, contributors, commits] = await Promise.all( + [infoRes, langsRes, tagsRes, contribRes, commitsRes].map(parseJson), + ); + + return { + info: { + ownerUsername: info.owner?.login ?? '', + ownerAvatar: info.owner?.avatar_url ?? '', + description: info.description ?? '', + url: info.html_url ?? '', + homepage: info.homepage ?? '', + language: info.language ?? '', + topics: info.topics ?? [], + license: info.license?.spdx_id ?? '', + isFork: info.fork ?? false, + isArchived: info.archived ?? false, + createdAt: info.created_at ?? '', + updatedAt: info.updated_at ?? '', + size: info.size ?? 0, + scarCount: info.stargazers_count ?? 0, + forksCount: info.forks_count ?? 0, + watchersCount: info.watchers_count ?? 0, + }, + languages: languages ?? {}, + versions: (tags ?? []).map((tag: any) => ({ + name: tag.name, + commit: tag.commit?.sha ?? '', + zipball: tag.zipball_url ?? '', + tarball: tag.tarball_url ?? '', + })), + contributors: (contributors ?? []).map((c: any) => ({ + username: c.login ?? '', + avatar: c.avatar_url ?? '', + contributions: c.contributions ?? 0, + })), + commits: (commits ?? []).map((c: any) => ({ + sha: c.sha ?? '', + authorName: c.commit?.author?.name ?? '', + authorDate: c.commit?.author?.date ?? '', + message: c.commit?.message ?? '', + authorUsername: c.author?.login ?? '', + authorAvatar: c.author?.avatar_url ?? '', + })), + }; +}; + +const fetchFromWorker = async (github: string): Promise => { + const res = await fetch(`https://repo-info.as93.workers.dev/${github}`); + if (!res.ok) return null; + return res.json(); +}; + export const fetchGitHubStats = async ( github: string, ): Promise => { - const endpoint = `https://repo-info.as93.workers.dev/${github}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching GitHub stats:', error); + const result = await fetchFromGitHub(github); + if (result) return result; + const fallback = await fetchFromWorker(github); + if (fallback) return fallback; + error('GitHub Stats', `Both direct API and worker failed for ${github}`); return null; + } catch (err) { + try { + return await fetchFromWorker(github); + } catch { + error('GitHub Stats', `All fetches failed for ${github}: ${err}`); + return null; + } } }; -// fetch(`https://repo-info.as93.workers.dev/${github}`).then((res) => res.json()); - export interface GitHubStatsResponse { info: { ownerUsername: string; @@ -34,17 +124,6 @@ export interface GitHubStatsResponse { languages: { [key: string]: number; }; - updates: Array<{ - type: string; - actor: { - username: string; - avatar: string; - }; - repo: string; - action: string; - createdAt: string; - number?: number; - }>; versions: Array<{ name: string; commit: string; diff --git a/web/src/utils/fetch-website-info.ts b/web/src/utils/fetch-website-info.ts index 1ddeb29..46be050 100644 --- a/web/src/utils/fetch-website-info.ts +++ b/web/src/utils/fetch-website-info.ts @@ -1,11 +1,18 @@ +import { error } from './logger'; + export const fetchWebsiteInfo = async ( url: string, ): Promise => { const endpoint = `https://site-info-fetch.as93.workers.dev/?url=${url}`; try { - return await fetch(endpoint).then((res) => res.json()); - } catch (error) { - console.error('Error fetching website info:', error); + const res = await fetch(endpoint); + if (!res.ok) { + error('Website', `HTTP ${res.status} for ${url} (${endpoint})`); + return null; + } + return await res.json(); + } catch (err) { + error('Website', `Network error for ${url}: ${err}`); return null; } }; diff --git a/web/src/utils/logger.ts b/web/src/utils/logger.ts new file mode 100644 index 0000000..55740d5 --- /dev/null +++ b/web/src/utils/logger.ts @@ -0,0 +1,48 @@ +type Level = 'warn' | 'error'; + +interface LogEntry { + level: Level; + source: string; + message: string; +} + +const entries: LogEntry[] = []; + +export const warn = (source: string, message: string) => { + console.warn(`[${source}] ${message}`); + entries.push({ level: 'warn', source, message }); +}; + +export const error = (source: string, message: string) => { + console.error(`[${source}] ${message}`); + entries.push({ level: 'error', source, message }); +}; + +export const printSummary = () => { + if (entries.length === 0) return; + + const grouped: Record = {}; + for (const entry of entries) { + if (!grouped[entry.source]) { + grouped[entry.source] = { errors: 0, warnings: 0, messages: [] }; + } + const group = grouped[entry.source]; + if (entry.level === 'error') group.errors++; + else group.warnings++; + group.messages.push(` ${entry.level.toUpperCase()}: ${entry.message}`); + } + + console.log('\n───────────── Build fetch summary ──────────────'); + for (const [source, { errors, warnings, messages }] of Object.entries(grouped)) { + const parts = []; + if (errors) parts.push(`${errors} error${errors > 1 ? 's' : ''}`); + if (warnings) parts.push(`${warnings} warning${warnings > 1 ? 's' : ''}`); + console.log(`[${source}] ${parts.join(', ')}`); + for (const msg of messages) { + console.log(msg); + } + } + console.log('────────────────────────────────────────────────\n'); + + entries.length = 0; +};