feat(ccusage): add CCUsage collection service for usage tracking#106
feat(ccusage): add CCUsage collection service for usage tracking#106
Conversation
- Add CCUsage configuration struct with optional enabled field - Implement CCUsage service that collects usage data hourly using bunx/npx - Integrate service into daemon to run when enabled - Split data collection (via ccusage CLI) and remote sending logic - Support both bunx and npx package runners with fallback
Summary of ChangesHello @AnnatarHe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a new feature for collecting Claude Code usage statistics. It provides a dedicated service that, when enabled through configuration, periodically gathers usage data from the system using available package runners ( Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
|
Claude finished @AnnatarHe's task —— View job Code Review for CCUsage Collection ServiceI've completed a thorough review of the CCUsage collection service implementation. Here's my comprehensive feedback: Todo List
✅ Positive Aspects
|
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new service for collecting Claude Code usage statistics. The implementation is well-structured, with a clear separation of concerns for data collection, service lifecycle management, and data submission. The changes to the configuration and daemon startup are appropriate.
My review includes several suggestions to improve maintainability, robustness, and consistency with the existing codebase. The key points are:
- Standardizing on a single logging library (
slog). - Improving type safety by using specific structs instead of
map[string]interface{}for known data structures. - Increasing robustness in how system information like the username is retrieved.
- Enhancing flexibility by making the collection interval configurable.
- Refactoring for better readability and to reduce code duplication.
Overall, this is a solid addition. Addressing these points will make the new service even more robust and easier to maintain.
| "os/user" | ||
| "time" | ||
|
|
||
| "github.com/sirupsen/logrus" |
There was a problem hiding this comment.
The project appears to be standardizing on slog for logging, as it's configured in cmd/daemon/main.go. This file introduces logrus, creating an inconsistency. To maintain a single logging framework for easier configuration and maintenance, please migrate the logging in this file from logrus to slog.
This will involve:
- Replacing the
logrusimport withlog/slog. - Updating log calls. For example:
logrus.Info("message")becomesslog.Info("message")logrus.Warnf("message: %v", err)becomesslog.Warn("message", slog.Any("error", err))
| "github.com/sirupsen/logrus" | |
| "log/slog" |
| go daemon.SocketTopicProccessor(msg) | ||
|
|
||
| // Start CCUsage service if enabled | ||
| if cfg.CCUsage != nil && cfg.CCUsage.Enabled != nil && *cfg.CCUsage.Enabled { |
There was a problem hiding this comment.
This condition to check if CCUsage is enabled is verbose and is duplicated in model/ccusage_service.go. To improve readability and avoid code duplication (Don't Repeat Yourself - DRY), add a helper method to the CCUsage struct.
In model/types.go:
func (c *CCUsage) IsEnabled() bool {
return c != nil && c.Enabled != nil && *c.Enabled
}Then you can simplify this line and the check in ccusage_service.go.
| if cfg.CCUsage != nil && cfg.CCUsage.Enabled != nil && *cfg.CCUsage.Enabled { | |
| if cfg.CCUsage.IsEnabled() { |
| Username string `json:"username" msgpack:"username"` | ||
| OS string `json:"os" msgpack:"os"` | ||
| OSVersion string `json:"osVersion" msgpack:"osVersion"` | ||
| Data map[string]interface{} `json:"data" msgpack:"data"` |
There was a problem hiding this comment.
Using map[string]interface{} for the Data field bypasses type safety. Define a specific struct for the ccusage command's output to make the code more robust and self-documenting.
For example, you could define a struct like this and use it here:
// CCUsageStats defines the structure of the data from the ccusage command
type CCUsageStats struct {
Completions int `json:"completions"`
Users int `json:"users"`
// ... other fields
}This would improve type safety and make the code easier to maintain.
| Data map[string]interface{} `json:"data" msgpack:"data"` | |
| Data CCUsageStats `json:"data" msgpack:"data"` |
| logrus.Info("Starting CCUsage collection service") | ||
|
|
||
| // Create a ticker for hourly collection | ||
| s.ticker = time.NewTicker(1 * time.Hour) |
There was a problem hiding this comment.
The collection interval is hardcoded to 1 hour. This reduces flexibility. Consider making this value configurable in config.toml under the [ccusage] section. This would allow operators to adjust the collection frequency without changing the code.
You would need to add a field to the CCUsage struct in model/types.go, for example:
// In model/types.go
type CCUsage struct {
Enabled *bool `toml:"enabled"`
CollectionInterval string `toml:"collectionInterval"` // e.g. "1h", "30m"
}interval := 1 * time.Hour
if s.config.CCUsage != nil && s.config.CCUsage.CollectionInterval != "" {
if parsedInterval, err := time.ParseDuration(s.config.CCUsage.CollectionInterval); err == nil {
interval = parsedInterval
} else {
logrus.Warnf("Invalid CCUsage.CollectionInterval '%s', using default 1h: %v", s.config.CCUsage.CollectionInterval, err)
}
}
s.ticker = time.NewTicker(interval)| username := os.Getenv("USER") | ||
| if username == "" { | ||
| currentUser, err := user.Current() | ||
| if err != nil { | ||
| logrus.Warnf("Failed to get username: %v", err) | ||
| username = "unknown" | ||
| } else { | ||
| username = currentUser.Username | ||
| } | ||
| } |
There was a problem hiding this comment.
Getting the username from the USER environment variable is not always reliable. The user.Current() function is a more robust, cross-platform method and should be the primary method for retrieving the username.
var username string
currentUser, err := user.Current()
if err != nil {
logrus.Warnf("Failed to get current user: %v, falling back to USER env var", err)
username = os.Getenv("USER")
} else {
username = currentUser.Username
}
if username == "" {
logrus.Warnf("Failed to get username")
username = "unknown"
}- Add CCUsageProjectDailyOutput struct for strongly typed JSON parsing - Replace generic map with specific type for better type safety - Auto-generated from actual ccusage command output structure
|
Claude finished @AnnatarHe's task —— View job Code Review for CCUsage Collection ServiceI've completed a thorough review of the CCUsage collection service implementation. Here's my comprehensive feedback: Todo List
✅ Positive Aspects
|
- Add SendGraphQLRequest helper function for consistent GraphQL operations - Implement getLastSyncTimestamp to fetch last sync time from server - Update collectData to support since parameter for incremental data collection - Refactor dotfile.go to use new SendGraphQLRequest helper - Reduce duplicate code and improve maintainability
|
Claude finished @AnnatarHe's task —— View job Claude Code Review: CCUsage Collection Service Todo List
✅ Positive Aspects
|
- Add generic GraphQLResponse[T] struct for consistent response handling - Add GraphQLError struct for proper error representation - Update ccusage and dotfile services to use generic response types - Add GraphQL error checking in SendGraphQLRequest - Reduce code duplication and improve type safety
|
Claude finished @AnnatarHe's task —— View job Claude Code Review: CCUsage Collection Service Todo List
✅ Positive Aspects
|
- Changed API path from /api/v1/ccusage to /api/v1/ccusage/batch - Transformed data structure to match server's CCUsageBatchPayload format - Updated response handling to include success count and failed projects - Added proper batch entry transformation for multiple projects and dates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Added new SendHTTPRequestJSON function for JSON-based HTTP requests - Marked SendHTTPRequest as deprecated (uses msgpack) for legacy support - Refactored SendGraphQLRequest to use SendHTTPRequestJSON internally - Updated ccusage service to use the new JSON-based function - Eliminated code duplication in GraphQL request handling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Claude finished @AnnatarHe's task —— View job Claude Code Review: CCUsage Collection Service Todo List
✅ Positive Aspects
|
…y hostname - Fix GraphQL query structure to match server schema (User.ccusage.lastSyncAt) - Add hostname parameter to getLastSyncTimestamp to filter CCUsage data by current host - Ensure incremental sync is host-specific to prevent cross-host data conflicts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Claude finished @AnnatarHe's task —— View job Code Review for CCUsage Collection ServiceTodo List
✅ Positive Aspects
|
- Changed lastSyncAt from int64 to string in GraphQL response - Parse RFC3339 timestamp format from server - Filter out timestamps before 2023 as invalid - Use time.Time instead of *int64 for better type safety 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Claude finished @AnnatarHe's task —— View job Code Review for CCUsage Collection ServiceTodo List
✅ Positive Aspects
|
Change timestamp logging from %d to %v for better readability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Claude finished @AnnatarHe's task —— View job Code Review for CCUsage Collection ServiceTodo List
✅ Positive Aspects
|
Description
This PR adds a new CCUsage collection service that tracks Claude Code usage statistics when enabled.
Changes
CCUsagestruct in config withEnabledfieldbunx ccusage daily --jsonornpx ccusage daily --json/api/v1/ccusageConfiguration
To enable CCUsage collection, add to your config.toml:
Implementation Details
bunxfirst, falls back tonpxif not availableTesting