Skip to content

Commit ce32440

Browse files
authored
feat: add WAKATIME_AI_STATS metric (#27)
2 parents c75703c + 089bb13 commit ce32440

14 files changed

Lines changed: 483 additions & 117 deletions

File tree

.github/workflows/ci.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Test
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version: '1.24'
22+
cache: true
23+
24+
- name: Check gofmt
25+
run: |
26+
unformatted=$(gofmt -l .)
27+
if [ -n "$unformatted" ]; then
28+
echo "The following files are not gofmt-ed:"
29+
echo "$unformatted"
30+
exit 1
31+
fi
32+
33+
- name: Vet
34+
run: go vet ./...
35+
36+
- name: Test
37+
run: go test -race ./...
38+
39+
lint:
40+
name: Lint
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- uses: actions/setup-go@v5
46+
with:
47+
go-version: '1.24'
48+
cache: true
49+
50+
- uses: golangci/golangci-lint-action@v7
51+
with:
52+
version: v2.1
53+
54+
docker:
55+
name: Docker build
56+
runs-on: ubuntu-latest
57+
steps:
58+
- uses: actions/checkout@v4
59+
60+
- name: Build image
61+
run: docker build -t github-stats:ci .

.golangci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: "2"
2+
3+
run:
4+
timeout: 5m
5+
6+
linters:
7+
default: none
8+
enable:
9+
- errcheck
10+
- govet
11+
- ineffassign
12+
- staticcheck
13+
- misspell
14+
exclusions:
15+
rules:
16+
- path: _test\.go
17+
linters:
18+
- errcheck

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,27 @@ Displays all languages you use with colorful badges showing percentages.
192192
![Java](https://img.shields.io/badge/Java-12.0%25-b07219?&logo=Java&labelColor=151b23)
193193
![Go](https://img.shields.io/badge/Go-2.8%25-00ADD8?&logo=Go&labelColor=151b23)
194194
195+
### 🤖 `WAKATIME_AI_STATS`
196+
197+
Shows AI vs human coding attribution from WakaTime, aggregated across all your projects in the configured range.
198+
199+
**Requirements:**
200+
- `WAKATIME_API_KEY` (required) — WakaTime must be tracking AI attribution (requires GenAI integration in your editor)
201+
- `WAKATIME_RANGE` (optional) — same range as `WAKATIME_SPENT_TIME`
202+
203+
**Example output:**
204+
205+
**🤖 My AI Footprint**
206+
```
207+
🤖 Generated by AI: 12,340 lines
208+
👤 Written by Hand: 8,721 lines
209+
📊 AI Contribution: 58.6%
210+
🔤 Tokens In / Out: 1.2M / 3.4M
211+
💬 Average Prompt: 142 chars
212+
```
213+
214+
> 💡 **Note:** The block is hidden entirely when no AI activity is reported (no GenAI integration, or zero AI usage in the range), so you won't see a section full of zeros. `Avg Prompt Length` is weighted by `ai_input_tokens` across projects.
215+
195216
### ⏱️ `WAKATIME_SPENT_TIME`
196217
197218
Shows detailed coding activity from WakaTime (requires WakaTime API key).
@@ -301,7 +322,7 @@ env:
301322
WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
302323
WAKATIME_DATA: "EDITORS,LANGUAGES,PROJECTS,OPERATING_SYSTEMS"
303324
WAKATIME_RANGE: "last_30_days"
304-
SHOW_METRICS: "COMMIT_TIMES_OF_DAY,COMMIT_DAYS_OF_WEEK,LANGUAGE_PER_REPO,LANGUAGES_AND_TOOLS,WAKATIME_SPENT_TIME,CODING_STREAK"
325+
SHOW_METRICS: "COMMIT_TIMES_OF_DAY,COMMIT_DAYS_OF_WEEK,LANGUAGE_PER_REPO,LANGUAGES_AND_TOOLS,WAKATIME_SPENT_TIME,CODING_STREAK,WAKATIME_AI_STATS"
305326
SHOW_LAST_UPDATE: "true"
306327
ONLY_MAIN_BRANCH: "true"
307328
PROGRESS_BAR_VERSION: "2"

cmd/git_test.go

Lines changed: 61 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,99 +8,99 @@ import (
88

99
func TestSanitizeError(t *testing.T) {
1010
tests := []struct {
11-
name string
12-
err error
13-
token string
14-
owner string
15-
expectedParts []string // Parts that should be in the result
11+
name string
12+
err error
13+
token string
14+
owner string
15+
expectedParts []string // Parts that should be in the result
1616
forbiddenParts []string // Parts that should NOT be in the result
1717
}{
1818
{
19-
name: "nil error returns nil",
20-
err: nil,
21-
token: "ghp_1234567890",
22-
owner: "testuser",
23-
expectedParts: nil,
19+
name: "nil error returns nil",
20+
err: nil,
21+
token: "ghp_1234567890",
22+
owner: "testuser",
23+
expectedParts: nil,
2424
forbiddenParts: nil,
2525
},
2626
{
27-
name: "token is redacted",
28-
err: errors.New("authentication failed with token ghp_1234567890"),
29-
token: "ghp_1234567890",
30-
owner: "testuser",
31-
expectedParts: []string{"[***]"},
27+
name: "token is redacted",
28+
err: errors.New("authentication failed with token ghp_1234567890"),
29+
token: "ghp_1234567890",
30+
owner: "testuser",
31+
expectedParts: []string{"[***]"},
3232
forbiddenParts: []string{"ghp_1234567890"},
3333
},
3434
{
35-
name: "owner/username is redacted",
36-
err: errors.New("failed to push to testuser/repo"),
37-
token: "ghp_1234567890",
38-
owner: "testuser",
39-
expectedParts: []string{"[***]"},
35+
name: "owner/username is redacted",
36+
err: errors.New("failed to push to testuser/repo"),
37+
token: "ghp_1234567890",
38+
owner: "testuser",
39+
expectedParts: []string{"[***]"},
4040
forbiddenParts: []string{"testuser"},
4141
},
4242
{
43-
name: "https URL is completely redacted",
44-
err: errors.New("failed to clone https://github.com/user/repo.git"),
45-
token: "",
46-
owner: "",
47-
expectedParts: []string{"[***]"},
43+
name: "https URL is completely redacted",
44+
err: errors.New("failed to clone https://github.com/user/repo.git"),
45+
token: "",
46+
owner: "",
47+
expectedParts: []string{"[***]"},
4848
forbiddenParts: []string{"https://github.com", "github.com", "/user/repo.git"},
4949
},
5050
{
51-
name: "http URL is completely redacted",
52-
err: errors.New("failed to fetch http://example.com/path/to/resource"),
53-
token: "",
54-
owner: "",
55-
expectedParts: []string{"[***]"},
51+
name: "http URL is completely redacted",
52+
err: errors.New("failed to fetch http://example.com/path/to/resource"),
53+
token: "",
54+
owner: "",
55+
expectedParts: []string{"[***]"},
5656
forbiddenParts: []string{"http://example.com", "example.com", "/path/to/resource"},
5757
},
5858
{
59-
name: "multiple URLs are redacted",
60-
err: errors.New("failed: https://github.com/repo1 and https://github.com/repo2"),
61-
token: "",
62-
owner: "",
63-
expectedParts: []string{"[***]"},
59+
name: "multiple URLs are redacted",
60+
err: errors.New("failed: https://github.com/repo1 and https://github.com/repo2"),
61+
token: "",
62+
owner: "",
63+
expectedParts: []string{"[***]"},
6464
forbiddenParts: []string{"github.com", "repo1", "repo2"},
6565
},
6666
{
67-
name: "URL with token in it is redacted",
68-
err: errors.New("failed to push to https://ghp_token123@github.com/user/repo.git"),
69-
token: "ghp_token123",
70-
owner: "user",
71-
expectedParts: []string{"[***]"},
67+
name: "URL with token in it is redacted",
68+
err: errors.New("failed to push to https://ghp_token123@github.com/user/repo.git"),
69+
token: "ghp_token123",
70+
owner: "user",
71+
expectedParts: []string{"[***]"},
7272
forbiddenParts: []string{"ghp_token123", "github.com", "user", "repo.git"},
7373
},
7474
{
75-
name: "complex error with token, owner, and URL",
76-
err: errors.New("git remote set-url failed for testuser: https://ghp_secret@github.com/testuser/myrepo.git"),
77-
token: "ghp_secret",
78-
owner: "testuser",
79-
expectedParts: []string{"[***]"},
75+
name: "complex error with token, owner, and URL",
76+
err: errors.New("git remote set-url failed for testuser: https://ghp_secret@github.com/testuser/myrepo.git"),
77+
token: "ghp_secret",
78+
owner: "testuser",
79+
expectedParts: []string{"[***]"},
8080
forbiddenParts: []string{"ghp_secret", "testuser", "github.com", "myrepo"},
8181
},
8282
{
83-
name: "error without sensitive data remains unchanged",
84-
err: errors.New("connection timeout"),
85-
token: "ghp_1234",
86-
owner: "user",
87-
expectedParts: []string{"connection timeout"},
83+
name: "error without sensitive data remains unchanged",
84+
err: errors.New("connection timeout"),
85+
token: "ghp_1234",
86+
owner: "user",
87+
expectedParts: []string{"connection timeout"},
8888
forbiddenParts: nil,
8989
},
9090
{
91-
name: "URL with query parameters is fully redacted",
92-
err: errors.New("failed: https://api.github.com/repos/user/repo?token=abc123"),
93-
token: "",
94-
owner: "",
95-
expectedParts: []string{"[***]"},
91+
name: "URL with query parameters is fully redacted",
92+
err: errors.New("failed: https://api.github.com/repos/user/repo?token=abc123"),
93+
token: "",
94+
owner: "",
95+
expectedParts: []string{"[***]"},
9696
forbiddenParts: []string{"api.github.com", "token=abc123", "user", "repo"},
9797
},
9898
{
99-
name: "empty token and owner still redacts URLs",
100-
err: errors.New("error with https://github.com/path"),
101-
token: "",
102-
owner: "",
103-
expectedParts: []string{"[***]"},
99+
name: "empty token and owner still redacts URLs",
100+
err: errors.New("error with https://github.com/path"),
101+
token: "",
102+
owner: "",
103+
expectedParts: []string{"[***]"},
104104
forbiddenParts: []string{"github.com"},
105105
},
106106
}
@@ -141,9 +141,6 @@ func TestSanitizeError(t *testing.T) {
141141
}
142142
}
143143

144-
145-
146-
147144
func TestSanitizeError_EdgeCases(t *testing.T) {
148145
tests := []struct {
149146
name string
@@ -282,7 +279,7 @@ func TestSanitizeError_PreservesErrorStructure(t *testing.T) {
282279
}
283280

284281
// Should still be an error type
285-
var err error = sanitized
282+
var err = sanitized
286283
if err == nil {
287284
t.Error("sanitized error should still implement error interface")
288285
}
@@ -298,7 +295,6 @@ func TestSanitizeError_PreservesErrorStructure(t *testing.T) {
298295
}
299296
}
300297

301-
302298
// TestSanitizeError_OrderOfOperations tests that sanitization happens in the correct order
303299
func TestSanitizeError_OrderOfOperations(t *testing.T) {
304300
// Test that token is replaced before URL regex runs
@@ -410,4 +406,3 @@ func TestSanitizeError_CaseSensitivity(t *testing.T) {
410406
t.Errorf("expected at least one redaction, got: %s", resultMsg)
411407
}
412408
}
413-

pkg/config/config.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
MetricCommitTimesOfDay = "COMMIT_TIMES_OF_DAY"
1717
MetricWakaTimeSpentTime = "WAKATIME_SPENT_TIME"
1818
MetricCodingStreak = "CODING_STREAK"
19+
MetricWakaTimeAIStats = "WAKATIME_AI_STATS"
1920
)
2021

2122
// Valid data types for WAKATIME_DATA
@@ -115,7 +116,8 @@ func Load() *Config {
115116
}
116117

117118
if ttl := os.Getenv("CACHE_TTL"); ttl != "" {
118-
fmt.Sscanf(ttl, "%d", &cfg.CacheTTL)
119+
// parse failure leaves CacheTTL at zero; applyDefaults() then fills the fallback
120+
_, _ = fmt.Sscanf(ttl, "%d", &cfg.CacheTTL)
119121
}
120122

121123
cfg.applyDefaults()
@@ -204,6 +206,7 @@ func (c *Config) Validate() error {
204206
MetricCommitTimesOfDay,
205207
MetricWakaTimeSpentTime,
206208
MetricCodingStreak,
209+
MetricWakaTimeAIStats,
207210
}
208211
for _, metric := range c.ShowMetrics {
209212
trimmed := strings.TrimSpace(metric)

pkg/config/config_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ func TestConfig_Validate(t *testing.T) {
2323
{
2424
name: "valid config with all settings",
2525
config: &Config{
26-
GitHubToken: "ghp_test123",
27-
WakaTimeAPIKey: "waka_test123",
28-
WakaTimeRange: "last_7_days",
29-
WakaTimeData: []string{"EDITORS", "LANGUAGES"},
30-
ShowMetrics: []string{"COMMIT_TIMES_OF_DAY", "LANGUAGE_PER_REPO"},
26+
GitHubToken: "ghp_test123",
27+
WakaTimeAPIKey: "waka_test123",
28+
WakaTimeRange: "last_7_days",
29+
WakaTimeData: []string{"EDITORS", "LANGUAGES"},
30+
ShowMetrics: []string{"COMMIT_TIMES_OF_DAY", "LANGUAGE_PER_REPO"},
3131
ProgressBarVersion: "2",
3232
},
3333
wantErr: false,
@@ -133,4 +133,3 @@ func TestConfig_Validate(t *testing.T) {
133133
})
134134
}
135135
}
136-

0 commit comments

Comments
 (0)