[이제와서 시작하는 GitHub 마스터하기 - 고급편 #9] GitHub 통계와 인사이트: 데이터로 보는 프로젝트
[이제와서 시작하는 GitHub 마스터하기 - 고급편 #9] GitHub 통계와 인사이트: 데이터로 보는 프로젝트
들어가며
“이제와서 시작하는 GitHub 마스터하기” 시리즈의 마지막 스무 번째 시간입니다. 이번에는 GitHub의 통계와 인사이트 기능을 활용하여 프로젝트의 건강 상태를 파악하고, 데이터 기반의 의사결정을 내리는 방법을 알아보겠습니다. 숫자와 그래프로 프로젝트의 진짜 모습을 들여다봅시다.
1. GitHub Insights 개요
Insights 탭 구성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
GitHub Insights:
Pulse:
- 최근 활동 요약
- 주간/월간 통계
- 활발한 기여자
Contributors:
- 기여자별 커밋 통계
- 추가/삭제 라인 수
- 기여 타임라인
Community:
- 커뮤니티 프로필
- 건강성 체크리스트
- 문서 상태
Traffic:
- 방문자 통계
- 인기 콘텐츠
- 참조 소스
Commits:
- 커밋 빈도
- 시간대별 분포
- 주간 패턴
Code frequency:
- 코드 추가/삭제 추세
- 프로젝트 성장률
Dependency graph:
- 의존성 시각화
- 보안 취약점
Network:
- Fork 네트워크
- 브랜치 관계
Forks:
- Fork 목록
- Fork 활동
2. Pulse - 프로젝트 활동 모니터링
Pulse 데이터 해석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pulse 주요 지표:
활동 요약 (지난 7일/30일):
- PR 열림/병합/닫힘
- 이슈 열림/닫힘
- 커밋 수
- 릴리스 수
활발한 기여자:
- 커밋 수 기준 상위 기여자
- 신규 기여자
- 리뷰어 활동
프로젝트 건강성 지표:
✅ 일일 커밋 수 > 5
✅ PR 평균 머지 시간 < 24시간
✅ 이슈 응답 시간 < 48시간
⚠️ 미해결 PR > 10개
❌ 30일 이상 된 이슈 존재
API로 Pulse 데이터 가져오기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// GitHub API를 사용한 활동 통계 수집
const { Octokit } = require('@octokit/rest');
async function getProjectPulse(owner, repo, days = 7) {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
const since = new Date();
since.setDate(since.getDate() - days);
// PR 통계
const { data: prs } = await octokit.pulls.list({
owner,
repo,
state: 'all',
since: since.toISOString(),
per_page: 100
});
// 이슈 통계
const { data: issues } = await octokit.issues.listForRepo({
owner,
repo,
state: 'all',
since: since.toISOString(),
per_page: 100
});
// 커밋 통계
const { data: commits } = await octokit.repos.listCommits({
owner,
repo,
since: since.toISOString(),
per_page: 100
});
return {
period: `${days} days`,
pullRequests: {
opened: prs.filter(pr => new Date(pr.created_at) > since).length,
merged: prs.filter(pr => pr.merged_at && new Date(pr.merged_at) > since).length,
closed: prs.filter(pr => pr.closed_at && new Date(pr.closed_at) > since).length
},
issues: {
opened: issues.filter(i => !i.pull_request && new Date(i.created_at) > since).length,
closed: issues.filter(i => !i.pull_request && i.closed_at && new Date(i.closed_at) > since).length
},
commits: commits.length,
contributors: [...new Set(commits.map(c => c.author?.login).filter(Boolean))].length
};
}
3. Contributors - 기여 분석
기여도 시각화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 기여자 통계 시각화
import requests
import matplotlib.pyplot as plt
import pandas as pd
from datetime import datetime, timedelta
def analyze_contributors(owner, repo, token):
headers = {'Authorization': f'token {token}'}
# 기여자 통계 가져오기
url = f'https://api.github.com/repos/{owner}/{repo}/contributors'
response = requests.get(url, headers=headers)
contributors = response.json()
# 데이터프레임 생성
df = pd.DataFrame([{
'login': c['login'],
'contributions': c['contributions'],
'avatar': c['avatar_url']
} for c in contributors[:20]]) # 상위 20명
# 시각화
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# 막대 그래프
ax1.barh(df['login'], df['contributions'])
ax1.set_xlabel('Contributions')
ax1.set_title('Top 20 Contributors')
ax1.invert_yaxis()
# 파이 차트
top5 = df.nlargest(5, 'contributions')
others = pd.DataFrame([{
'login': 'Others',
'contributions': df.iloc[5:]['contributions'].sum()
}])
pie_data = pd.concat([top5, others])
ax2.pie(pie_data['contributions'], labels=pie_data['login'], autopct='%1.1f%%')
ax2.set_title('Contribution Distribution')
plt.tight_layout()
plt.savefig('contributors_analysis.png')
return df
# 기여 패턴 분석
def analyze_contribution_patterns(owner, repo, token):
headers = {'Authorization': f'token {token}'}
# 최근 100개 커밋 분석
url = f'https://api.github.com/repos/{owner}/{repo}/commits'
response = requests.get(url, headers=headers, params={'per_page': 100})
commits = response.json()
# 시간대별 분석
hours = {}
days = {}
for commit in commits:
date = datetime.strptime(
commit['commit']['author']['date'],
'%Y-%m-%dT%H:%M:%SZ'
)
hours[date.hour] = hours.get(date.hour, 0) + 1
days[date.weekday()] = days.get(date.weekday(), 0) + 1
# 히트맵 생성
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
# 시간대별 커밋
ax1.bar(range(24), [hours.get(h, 0) for h in range(24)])
ax1.set_xlabel('Hour of Day')
ax1.set_ylabel('Commits')
ax1.set_title('Commit Activity by Hour')
# 요일별 커밋
day_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
ax2.bar(day_names, [days.get(d, 0) for d in range(7)])
ax2.set_xlabel('Day of Week')
ax2.set_ylabel('Commits')
ax2.set_title('Commit Activity by Day')
plt.tight_layout()
plt.savefig('contribution_patterns.png')
기여자 성장 추적
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 신규 기여자 추적
async function trackNewContributors(owner, repo) {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
const contributors = new Map();
const monthlyNew = {};
// 모든 커밋 가져오기 (페이지네이션)
let page = 1;
let hasMore = true;
while (hasMore) {
const { data: commits } = await octokit.repos.listCommits({
owner,
repo,
per_page: 100,
page: page++
});
if (commits.length === 0) {
hasMore = false;
break;
}
for (const commit of commits) {
const author = commit.author?.login;
if (!author) continue;
const date = new Date(commit.commit.author.date);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!contributors.has(author)) {
contributors.set(author, date);
monthlyNew[monthKey] = (monthlyNew[monthKey] || 0) + 1;
}
}
}
return {
totalContributors: contributors.size,
monthlyNewContributors: monthlyNew,
recentNewContributors: Array.from(contributors.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([login, firstCommit]) => ({ login, firstCommit }))
};
}
4. Traffic - 방문자 분석
트래픽 데이터 수집
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 트래픽 통계 (관리자 권한 필요)
async function getTrafficStats(owner, repo) {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
try {
// 페이지 뷰
const { data: views } = await octokit.repos.getViews({
owner,
repo,
per: 'day'
});
// 클론 통계
const { data: clones } = await octokit.repos.getClones({
owner,
repo,
per: 'day'
});
// 인기 경로
const { data: paths } = await octokit.repos.getTopPaths({
owner,
repo
});
// 참조 소스
const { data: referrers } = await octokit.repos.getTopReferrers({
owner,
repo
});
return {
views: {
total: views.count,
unique: views.uniques,
daily: views.views
},
clones: {
total: clones.count,
unique: clones.uniques,
daily: clones.clones
},
popularContent: paths.map(p => ({
path: p.path,
title: p.title,
views: p.count,
unique: p.uniques
})),
topReferrers: referrers.map(r => ({
referrer: r.referrer,
views: r.count,
unique: r.uniques
}))
};
} catch (error) {
console.error('Error fetching traffic stats:', error);
return null;
}
}
트래픽 시각화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 트래픽 데이터 시각화
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
def visualize_traffic(traffic_data):
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
# 페이지 뷰 추세
dates = [datetime.strptime(v['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
for v in traffic_data['views']['daily']]
views = [v['count'] for v in traffic_data['views']['daily']]
unique_views = [v['uniques'] for v in traffic_data['views']['daily']]
ax1.plot(dates, views, label='Total Views', marker='o')
ax1.plot(dates, unique_views, label='Unique Visitors', marker='s')
ax1.set_title('Page Views Over Time')
ax1.set_xlabel('Date')
ax1.set_ylabel('Views')
ax1.legend()
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
# 클론 통계
clone_dates = [datetime.strptime(c['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
for c in traffic_data['clones']['daily']]
clones = [c['count'] for c in traffic_data['clones']['daily']]
unique_clones = [c['uniques'] for c in traffic_data['clones']['daily']]
ax2.plot(clone_dates, clones, label='Total Clones', marker='o', color='green')
ax2.plot(clone_dates, unique_clones, label='Unique Cloners', marker='s', color='darkgreen')
ax2.set_title('Repository Clones Over Time')
ax2.set_xlabel('Date')
ax2.set_ylabel('Clones')
ax2.legend()
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
plt.setp(ax2.xaxis.get_majorticklabels(), rotation=45)
# 인기 콘텐츠
paths = [p['path'][:30] + '...' if len(p['path']) > 30 else p['path']
for p in traffic_data['popularContent'][:10]]
path_views = [p['views'] for p in traffic_data['popularContent'][:10]]
ax3.barh(paths, path_views)
ax3.set_xlabel('Views')
ax3.set_title('Top 10 Popular Content')
ax3.invert_yaxis()
# 참조 소스
referrers = [r['referrer'] for r in traffic_data['topReferrers'][:10]]
referrer_views = [r['views'] for r in traffic_data['topReferrers'][:10]]
ax4.pie(referrer_views, labels=referrers, autopct='%1.1f%%')
ax4.set_title('Traffic Sources')
plt.tight_layout()
plt.savefig('traffic_analysis.png', dpi=300, bbox_inches='tight')
5. Code Frequency - 코드 변화 추적
코드 변화 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 코드 빈도 분석
async function analyzeCodeFrequency(owner, repo) {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
// 코드 빈도 데이터
const { data: codeFrequency } = await octokit.repos.getCodeFrequencyStats({
owner,
repo
});
// 주간 데이터 처리
const weeklyStats = codeFrequency.map(week => ({
week: new Date(week[0] * 1000),
additions: week[1],
deletions: Math.abs(week[2]),
net: week[1] + week[2]
}));
// 프로젝트 성장 지표
const totalAdditions = weeklyStats.reduce((sum, w) => sum + w.additions, 0);
const totalDeletions = weeklyStats.reduce((sum, w) => sum + w.deletions, 0);
const netGrowth = totalAdditions - totalDeletions;
// 활동 패턴 분석
const last12Weeks = weeklyStats.slice(-12);
const avgWeeklyAdditions = last12Weeks.reduce((sum, w) => sum + w.additions, 0) / 12;
const avgWeeklyDeletions = last12Weeks.reduce((sum, w) => sum + w.deletions, 0) / 12;
// 리팩토링 주간 찾기 (삭제가 추가보다 많은 주)
const refactoringWeeks = weeklyStats.filter(w => w.deletions > w.additions);
return {
summary: {
totalAdditions,
totalDeletions,
netGrowth,
growthRate: ((netGrowth / totalAdditions) * 100).toFixed(2) + '%'
},
recentActivity: {
avgWeeklyAdditions: Math.round(avgWeeklyAdditions),
avgWeeklyDeletions: Math.round(avgWeeklyDeletions),
trend: avgWeeklyAdditions > avgWeeklyDeletions ? 'growing' : 'stabilizing'
},
refactoringWeeks: refactoringWeeks.length,
weeklyData: weeklyStats
};
}
코드 품질 메트릭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 코드 품질 지표 계산
def calculate_code_metrics(repo_path):
import os
import re
from pathlib import Path
metrics = {
'total_files': 0,
'total_lines': 0,
'code_lines': 0,
'comment_lines': 0,
'blank_lines': 0,
'languages': {},
'file_sizes': []
}
# 파일 확장자별 언어 매핑
lang_map = {
'.py': 'Python',
'.js': 'JavaScript',
'.ts': 'TypeScript',
'.java': 'Java',
'.go': 'Go',
'.rs': 'Rust',
'.cpp': 'C++',
'.c': 'C',
'.rb': 'Ruby',
'.php': 'PHP'
}
for root, dirs, files in os.walk(repo_path):
# .git 디렉토리 제외
dirs[:] = [d for d in dirs if d != '.git']
for file in files:
file_path = os.path.join(root, file)
ext = Path(file).suffix.lower()
if ext in lang_map:
metrics['total_files'] += 1
lang = lang_map[ext]
metrics['languages'][lang] = metrics['languages'].get(lang, 0) + 1
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
metrics['total_lines'] += len(lines)
metrics['file_sizes'].append(os.path.getsize(file_path))
for line in lines:
line = line.strip()
if not line:
metrics['blank_lines'] += 1
elif line.startswith('#') or line.startswith('//') or line.startswith('/*'):
metrics['comment_lines'] += 1
else:
metrics['code_lines'] += 1
except:
pass
# 추가 메트릭 계산
if metrics['total_files'] > 0:
metrics['avg_file_size'] = sum(metrics['file_sizes']) / metrics['total_files']
metrics['comment_ratio'] = (metrics['comment_lines'] / metrics['code_lines'] * 100) if metrics['code_lines'] > 0 else 0
return metrics
6. Community Standards - 커뮤니티 건강성
커뮤니티 프로필 체크
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 커뮤니티 건강성 평가
async function evaluateCommunityHealth(owner, repo) {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
const healthChecks = {
description: false,
readme: false,
license: false,
contributing: false,
codeOfConduct: false,
issueTemplates: false,
prTemplate: false,
security: false,
funding: false
};
// 저장소 정보
const { data: repoData } = await octokit.repos.get({ owner, repo });
healthChecks.description = !!repoData.description;
healthChecks.license = !!repoData.license;
// 커뮤니티 프로필
try {
const { data: community } = await octokit.repos.getCommunityProfileMetrics({
owner,
repo
});
healthChecks.readme = !!community.files.readme;
healthChecks.contributing = !!community.files.contributing;
healthChecks.codeOfConduct = !!community.files.code_of_conduct;
healthChecks.license = !!community.files.license;
healthChecks.issueTemplates = !!community.files.issue_template;
healthChecks.prTemplate = !!community.files.pull_request_template;
} catch (error) {
console.error('Error fetching community profile:', error);
}
// 보안 정책
try {
await octokit.repos.getContent({
owner,
repo,
path: 'SECURITY.md'
});
healthChecks.security = true;
} catch (error) {
// 파일이 없음
}
// 점수 계산
const totalChecks = Object.keys(healthChecks).length;
const passedChecks = Object.values(healthChecks).filter(v => v).length;
const healthScore = Math.round((passedChecks / totalChecks) * 100);
return {
checks: healthChecks,
score: healthScore,
grade: healthScore >= 90 ? 'A' :
healthScore >= 80 ? 'B' :
healthScore >= 70 ? 'C' :
healthScore >= 60 ? 'D' : 'F',
recommendations: Object.entries(healthChecks)
.filter(([_, passed]) => !passed)
.map(([check, _]) => getRecommendation(check))
};
}
function getRecommendation(check) {
const recommendations = {
description: "Add a clear description to help others understand your project",
readme: "Create a README.md file with project documentation",
license: "Add a LICENSE file to clarify how others can use your code",
contributing: "Create CONTRIBUTING.md to guide potential contributors",
codeOfConduct: "Add CODE_OF_CONDUCT.md to foster a welcoming community",
issueTemplates: "Add issue templates in .github/ISSUE_TEMPLATE/",
prTemplate: "Create .github/pull_request_template.md",
security: "Add SECURITY.md to explain security policies",
funding: "Consider adding .github/FUNDING.yml for sponsorship"
};
return recommendations[check] || `Add ${check} to improve community standards`;
}
7. 프로젝트 대시보드 만들기
종합 대시보드 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!-- dashboard.html -->
<!DOCTYPE html>
<html>
<head>
<title>GitHub Project Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
padding: 20px;
}
.metric-card {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 16px;
}
.metric-value {
font-size: 2em;
font-weight: bold;
color: #0366d6;
}
.metric-label {
color: #586069;
font-size: 14px;
}
.chart-container {
position: relative;
height: 300px;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>GitHub Project Analytics Dashboard</h1>
<div class="dashboard">
<!-- 주요 메트릭 카드 -->
<div class="metric-card">
<div class="metric-label">Total Stars</div>
<div class="metric-value" id="stars">-</div>
</div>
<div class="metric-card">
<div class="metric-label">Open Issues</div>
<div class="metric-value" id="issues">-</div>
</div>
<div class="metric-card">
<div class="metric-label">Open PRs</div>
<div class="metric-value" id="prs">-</div>
</div>
<div class="metric-card">
<div class="metric-label">Contributors</div>
<div class="metric-value" id="contributors">-</div>
</div>
</div>
<!-- 차트 영역 -->
<div class="dashboard">
<div class="metric-card">
<h3>Commit Activity</h3>
<div class="chart-container">
<canvas id="commitChart"></canvas>
</div>
</div>
<div class="metric-card">
<h3>Issue Resolution Time</h3>
<div class="chart-container">
<canvas id="issueChart"></canvas>
</div>
</div>
<div class="metric-card">
<h3>Code Frequency</h3>
<div class="chart-container">
<canvas id="codeChart"></canvas>
</div>
</div>
<div class="metric-card">
<h3>Community Health</h3>
<div class="chart-container">
<canvas id="healthChart"></canvas>
</div>
</div>
</div>
<script src="dashboard.js"></script>
</body>
</html>
대시보드 데이터 수집
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// dashboard.js
class GitHubDashboard {
constructor(owner, repo, token) {
this.owner = owner;
this.repo = repo;
this.token = token;
this.baseUrl = 'https://api.github.com';
}
async fetchData() {
const headers = {
'Authorization': `token ${this.token}`,
'Accept': 'application/vnd.github.v3+json'
};
// 기본 저장소 정보
const repoData = await fetch(
`${this.baseUrl}/repos/${this.owner}/${this.repo}`,
{ headers }
).then(r => r.json());
// 이슈 통계
const issues = await fetch(
`${this.baseUrl}/repos/${this.owner}/${this.repo}/issues?state=open`,
{ headers }
).then(r => r.json());
// PR 통계
const prs = await fetch(
`${this.baseUrl}/repos/${this.owner}/${this.repo}/pulls?state=open`,
{ headers }
).then(r => r.json());
// 기여자 수
const contributorsResponse = await fetch(
`${this.baseUrl}/repos/${this.owner}/${this.repo}/contributors?per_page=1`,
{ headers }
);
const contributorCount = this.extractTotalCount(contributorsResponse);
// 메트릭 업데이트
document.getElementById('stars').textContent = repoData.stargazers_count.toLocaleString();
document.getElementById('issues').textContent = issues.length;
document.getElementById('prs').textContent = prs.length;
document.getElementById('contributors').textContent = contributorCount;
// 차트 생성
await this.createCharts();
}
extractTotalCount(response) {
const linkHeader = response.headers.get('Link');
if (!linkHeader) return 1;
const matches = linkHeader.match(/page=(\d+)>; rel="last"/);
return matches ? parseInt(matches[1]) : 1;
}
async createCharts() {
// 커밋 활동 차트
const commitStats = await this.getCommitActivity();
this.createCommitChart(commitStats);
// 이슈 해결 시간 차트
const issueStats = await this.getIssueStats();
this.createIssueChart(issueStats);
// 코드 빈도 차트
const codeStats = await this.getCodeFrequency();
this.createCodeChart(codeStats);
// 커뮤니티 건강성 차트
const healthScore = await this.getCommunityHealth();
this.createHealthChart(healthScore);
}
createCommitChart(data) {
const ctx = document.getElementById('commitChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.week),
datasets: [{
label: 'Commits per Week',
data: data.map(d => d.total),
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
createHealthChart(score) {
const ctx = document.getElementById('healthChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Health Score', 'Remaining'],
datasets: [{
data: [score, 100 - score],
backgroundColor: [
score >= 80 ? 'rgb(75, 192, 75)' :
score >= 60 ? 'rgb(255, 205, 86)' : 'rgb(255, 99, 132)',
'rgb(201, 203, 207)'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
title: {
display: true,
text: `${score}% Healthy`
}
}
}
});
}
}
// 대시보드 초기화
const dashboard = new GitHubDashboard('owner', 'repo', 'your-token');
dashboard.fetchData();
8. 자동화된 리포트 생성
주간 리포트 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# weekly_report.py
import os
from datetime import datetime, timedelta
from github import Github
import matplotlib.pyplot as plt
from reportlab.lib.pagesizes import letter, A4
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
class GitHubWeeklyReport:
def __init__(self, token, owner, repo):
self.g = Github(token)
self.repo = self.g.get_repo(f"{owner}/{repo}")
self.owner = owner
self.repo_name = repo
self.week_ago = datetime.now() - timedelta(days=7)
def generate_report(self):
# 데이터 수집
metrics = self.collect_metrics()
# PDF 생성
filename = f"weekly_report_{datetime.now().strftime('%Y%m%d')}.pdf"
doc = SimpleDocTemplate(filename, pagesize=A4)
story = []
styles = getSampleStyleSheet()
# 제목
title = Paragraph(
f"Weekly Report: {self.owner}/{self.repo_name}",
styles['Title']
)
story.append(title)
story.append(Spacer(1, 12))
# 날짜
date_range = Paragraph(
f"Period: {self.week_ago.strftime('%Y-%m-%d')} to {datetime.now().strftime('%Y-%m-%d')}",
styles['Normal']
)
story.append(date_range)
story.append(Spacer(1, 20))
# 주요 메트릭 테이블
metrics_data = [
['Metric', 'Value', 'Change'],
['Stars', metrics['stars'], metrics['star_change']],
['Open Issues', metrics['open_issues'], metrics['issue_change']],
['Open PRs', metrics['open_prs'], metrics['pr_change']],
['Commits', metrics['commits_week'], 'N/A'],
['Contributors', metrics['contributors_week'], 'N/A']
]
metrics_table = Table(metrics_data)
metrics_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 14),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
story.append(metrics_table)
story.append(Spacer(1, 20))
# 활동 요약
activity_title = Paragraph("Activity Summary", styles['Heading2'])
story.append(activity_title)
story.append(Spacer(1, 12))
activity_text = f"""
This week, the repository had {metrics['commits_week']} commits from
{metrics['contributors_week']} contributors. {metrics['issues_closed']} issues
were closed and {metrics['prs_merged']} pull requests were merged.
"""
story.append(Paragraph(activity_text, styles['Normal']))
# 차트 추가
self.create_activity_charts(metrics)
story.append(Spacer(1, 20))
story.append(Image('weekly_activity.png', width=400, height=300))
# PDF 생성
doc.build(story)
print(f"Report generated: {filename}")
def collect_metrics(self):
metrics = {}
# 기본 메트릭
metrics['stars'] = self.repo.stargazers_count
metrics['open_issues'] = self.repo.open_issues_count
# PR 수
open_prs = len(list(self.repo.get_pulls(state='open')))
metrics['open_prs'] = open_prs
# 주간 활동
commits_week = 0
contributors_week = set()
for commit in self.repo.get_commits(since=self.week_ago):
commits_week += 1
if commit.author:
contributors_week.add(commit.author.login)
metrics['commits_week'] = commits_week
metrics['contributors_week'] = len(contributors_week)
# 이슈 통계
issues_closed = 0
for issue in self.repo.get_issues(state='closed', since=self.week_ago):
if not issue.pull_request:
issues_closed += 1
metrics['issues_closed'] = issues_closed
# PR 통계
prs_merged = 0
for pr in self.repo.get_pulls(state='closed', sort='updated', direction='desc'):
if pr.merged and pr.merged_at > self.week_ago:
prs_merged += 1
metrics['prs_merged'] = prs_merged
# 변화량 계산 (이전 주 대비)
# 실제로는 이전 데이터를 저장하고 비교해야 함
metrics['star_change'] = '+12'
metrics['issue_change'] = '-3'
metrics['pr_change'] = '-2'
return metrics
def create_activity_charts(self, metrics):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
# 활동 유형별 차트
activities = ['Commits', 'Issues Closed', 'PRs Merged']
values = [metrics['commits_week'], metrics['issues_closed'], metrics['prs_merged']]
ax1.bar(activities, values, color=['blue', 'green', 'orange'])
ax1.set_title('Weekly Activity')
ax1.set_ylabel('Count')
# 기여자 활동
# 실제로는 기여자별 커밋 수를 계산해야 함
contributors = ['User1', 'User2', 'User3', 'Others']
contributions = [15, 12, 8, metrics['commits_week'] - 35]
ax2.pie(contributions, labels=contributors, autopct='%1.1f%%')
ax2.set_title('Contribution Distribution')
plt.tight_layout()
plt.savefig('weekly_activity.png', dpi=150, bbox_inches='tight')
plt.close()
9. 실시간 모니터링
실시간 대시보드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 실시간 GitHub 이벤트 모니터링
class GitHubRealtimeMonitor {
constructor(owner, repo, token) {
this.owner = owner;
this.repo = repo;
this.token = token;
this.eventSource = null;
this.events = [];
}
startMonitoring() {
// GitHub Events API 폴링
this.pollEvents();
setInterval(() => this.pollEvents(), 60000); // 1분마다
// WebSocket 연결 (GitHub Enterprise의 경우)
// this.connectWebSocket();
}
async pollEvents() {
try {
const response = await fetch(
`https://api.github.com/repos/${this.owner}/${this.repo}/events`,
{
headers: {
'Authorization': `token ${this.token}`,
'Accept': 'application/vnd.github.v3+json'
}
}
);
const events = await response.json();
// 새 이벤트 처리
for (const event of events) {
if (!this.events.find(e => e.id === event.id)) {
this.events.unshift(event);
this.handleNewEvent(event);
}
}
// 오래된 이벤트 제거
this.events = this.events.slice(0, 100);
} catch (error) {
console.error('Error polling events:', error);
}
}
handleNewEvent(event) {
const notification = this.createNotification(event);
if (notification) {
this.displayNotification(notification);
this.updateDashboard(event);
}
}
createNotification(event) {
const actor = event.actor.login;
const time = new Date(event.created_at).toLocaleTimeString();
switch (event.type) {
case 'PushEvent':
const commits = event.payload.commits.length;
return {
type: 'push',
message: `${actor} pushed ${commits} commit(s)`,
time: time,
icon: '📤'
};
case 'IssuesEvent':
return {
type: 'issue',
message: `${actor} ${event.payload.action} issue #${event.payload.issue.number}`,
time: time,
icon: '🐛'
};
case 'PullRequestEvent':
return {
type: 'pr',
message: `${actor} ${event.payload.action} PR #${event.payload.pull_request.number}`,
time: time,
icon: '🔀'
};
case 'WatchEvent':
return {
type: 'star',
message: `${actor} starred the repository`,
time: time,
icon: '⭐'
};
default:
return null;
}
}
displayNotification(notification) {
// 브라우저 알림
if (Notification.permission === 'granted') {
new Notification(`${notification.icon} ${this.repo}`, {
body: notification.message,
icon: '/github-icon.png'
});
}
// 대시보드에 추가
const feed = document.getElementById('activity-feed');
const item = document.createElement('div');
item.className = `feed-item ${notification.type}`;
item.innerHTML = `
<span class="icon">${notification.icon}</span>
<span class="message">${notification.message}</span>
<span class="time">${notification.time}</span>
`;
feed.insertBefore(item, feed.firstChild);
}
updateDashboard(event) {
// 실시간 메트릭 업데이트
switch (event.type) {
case 'PushEvent':
this.incrementMetric('commits-today');
break;
case 'IssuesEvent':
if (event.payload.action === 'opened') {
this.incrementMetric('issues-opened');
} else if (event.payload.action === 'closed') {
this.incrementMetric('issues-closed');
}
break;
case 'PullRequestEvent':
if (event.payload.action === 'opened') {
this.incrementMetric('prs-opened');
} else if (event.payload.action === 'closed' && event.payload.pull_request.merged) {
this.incrementMetric('prs-merged');
}
break;
}
}
incrementMetric(metricId) {
const element = document.getElementById(metricId);
if (element) {
const current = parseInt(element.textContent) || 0;
element.textContent = current + 1;
// 애니메이션 효과
element.classList.add('updated');
setTimeout(() => element.classList.remove('updated'), 1000);
}
}
}
10. 인사이트 활용 모범 사례
데이터 기반 의사결정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
프로젝트 건강성 체크리스트:
활동성:
✅ 주간 커밋 수 > 10
✅ 활성 기여자 수 > 3
✅ 이슈 응답 시간 < 24시간
⚠️ PR 리뷰 시간 > 48시간
코드 품질:
✅ 테스트 커버리지 > 80%
✅ 코드 리뷰 완료율 100%
⚠️ 기술 부채 증가 중
커뮤니티:
✅ 문서화 완료
✅ 기여 가이드 존재
⚠️ 이슈 템플릿 미비
성장성:
✅ 스타 증가율 > 5%/월
✅ 신규 기여자 > 2명/월
⚠️ Fork 활용도 < 10%
개선 액션 플랜
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
## 데이터 기반 개선 계획
### 1. 응답 시간 개선
- 현재: 평균 72시간
- 목표: 24시간 이내
- 방법:
- 이슈 트리아지 봇 도입
- 주간 이슈 리뷰 미팅
- 자동 레이블링 시스템
### 2. 기여자 확대
- 현재: 월 평균 2명
- 목표: 월 평균 5명
- 방법:
- "good first issue" 레이블 활용
- 상세한 기여 가이드 작성
- 멘토링 프로그램 운영
### 3. 코드 품질 향상
- 현재: 테스트 커버리지 65%
- 목표: 85% 이상
- 방법:
- PR 병합 조건에 테스트 추가
- 코드 리뷰 체크리스트
- 정기적인 리팩토링 스프린트
마무리
GitHub Insights와 Analytics를 활용하면 프로젝트의 진짜 모습을 볼 수 있습니다.
핵심 포인트:
- 데이터 기반 프로젝트 관리
- 커뮤니티 건강성 모니터링
- 기여 패턴 분석
- 성장 지표 추적
- 자동화된 리포팅
단순히 코드를 작성하는 것을 넘어, 데이터를 통해 더 나은 프로젝트를 만들어가세요!
20편에 걸친 GitHub 입문 시리즈를 마칩니다. 이 시리즈가 여러분의 GitHub 활용 능력을 한 단계 높이는 데 도움이 되었기를 바랍니다. Happy Coding! 🚀
📚 GitHub 마스터하기 시리즈
🌱 기초편 (입문자)
💼 실전편 (중급자)
🚀 고급편 (전문가)
- GitHub Actions 입문
- Actions 고급 활용
- Webhooks와 API
- GitHub Apps 개발
- 보안 기능
- GitHub Packages
- Codespaces
- GitHub CLI
- [통계와 인사이트] (현재 글)(/posts/github-advanced-09-insights-and-analytics/)
🏆 심화편 (전문가+)
GitHub 입문 시리즈를 읽어주셔서 감사합니다!
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.