Skip to content

Conversation

@ryoppippi
Copy link
Owner

@ryoppippi ryoppippi commented Jun 12, 2025

Summary

This PR refactors data aggregation logic by extracting common functionality into helper functions and then merging them back into data-loader.ts for better maintainability and performance.

Key Changes

  • Extracted and merged data aggregation helpers: Created helper functions for common operations like aggregateByModel, aggregateModelBreakdowns, calculateTotals, filterByDateRange, and duplicate detection
  • Improved code organization: Reduced duplication across loadDailyUsageData, loadSessionData, and loadMonthlyUsageData functions
  • Enhanced type safety: Fixed TypeScript unsafe type issues by improving generic constraints and hoisting default objects
  • Better date filtering: Made date filtering more robust to handle ISO timestamp formats properly
  • Unified duplicate detection: Consistent use of isDuplicateEntry and markAsProcessed helper functions across all data loading functions

Technical Details

  • All helper functions are consolidated in data-loader.ts for optimal performance
  • Maintained backward compatibility with existing data formats
  • All existing tests pass with improved test coverage
  • Fixed linting and type checking issues identified in reviews

Review Feedback Addressed

  • ✅ Fixed TypeScript type safety issues with proper generic constraints
  • ✅ Improved date filtering robustness for various timestamp formats
  • ✅ Unified duplicate detection logic across all data loading functions
  • ✅ Removed unnecessary .ts extensions from imports
  • ✅ Enhanced code organization and maintainability

Test Results

All 117 tests pass, including:

  • Data aggregation and filtering functionality
  • Cost calculation with multiple modes (auto/calculate/display)
  • Deduplication across sessions and files
  • Support for Claude 4 models (Sonnet and Opus)

Create a new module to house common data aggregation functions that will
be used to reduce code duplication in data-loader.ts. This module includes:
- aggregateByModel() for model-based aggregation
- aggregateModelBreakdowns() for breakdown aggregation
- createModelBreakdowns() for formatting
- calculateTotals() for token/cost totals
- filterByDateRange() for date filtering
- isDuplicateEntry() and markAsProcessed() for deduplication
- extractUniqueModels() for unique model extraction
Replace duplicate code in data-loader.ts with the new helper functions
from data-aggregation module:
- Use aggregateByModel() instead of inline model aggregation logic
- Use createModelBreakdowns() for consistent breakdown formatting
- Use calculateTotals() instead of inline reduce operations
- Use filterByDateRange() for date filtering
- Use isDuplicateEntry() and markAsProcessed() for deduplication
- Use extractUniqueModels() for model list extraction
- Use aggregateModelBreakdowns() in loadMonthlyUsageData

This reduces code duplication by ~195 lines while maintaining the same
functionality and improving maintainability.
@ryoppippi ryoppippi requested a review from Copilot June 12, 2025 11:55
@coderabbitai
Copy link

coderabbitai bot commented Jun 12, 2025

Walkthrough

The changes refactor src/data-loader.ts by extracting aggregation, filtering, and deduplication logic into dedicated utility functions. Core data loading functions now delegate these operations to helpers, resulting in modularized and clearer code. No changes were made to the signatures or exports; all modifications are internal and structural.

Changes

File(s) Change Summary
src/data-loader.ts Refactored: Extracted aggregation, filtering, and deduplication logic into reusable helper functions; updated main functions to use these helpers; adjusted import paths for consistency. No changes to exports or public interfaces.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant DataLoader
    participant Helpers

    Caller->>DataLoader: loadDailyUsageData / loadSessionData / loadMonthlyUsageData
    DataLoader->>Helpers: filterByDateRange
    DataLoader->>Helpers: isDuplicateEntry / markAsProcessed
    DataLoader->>Helpers: aggregateByModel / aggregateModelBreakdowns
    DataLoader->>Helpers: createModelBreakdowns
    DataLoader->>Helpers: calculateTotals
    DataLoader->>Helpers: extractUniqueModels
    DataLoader-->>Caller: Aggregated and filtered usage data
Loading

Possibly related PRs

  • ryoppippi/ccusage#58: Refactors src/data-loader.ts to modularize deduplication and file sorting logic, related through shared focus on deduplication but addresses different aspects.

Poem

In the warren of code, helpers now leap,
Aggregation and filters no longer run deep.
Deduplication’s a breeze,
With helpers to please—
A modular meadow where logic can sleep!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jun 12, 2025

Open in StackBlitz

npm i https://pkg.pr.new/ryoppippi/ccusage@65

commit: fbb507c

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @ryoppippi, 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 aims to improve code organization and reduce duplication by extracting common data aggregation, filtering, and utility functions from data-loader.ts into a dedicated data-aggregation.ts module. The existing data loading functions (loadDailyUsageData, loadSessionData, loadMonthlyUsageData) were updated to utilize these new helper functions.

Highlights

  • New Module: Introduced a new module src/data-aggregation.ts to consolidate common data processing and aggregation logic.
  • Refactoring data-loader.ts: Replaced duplicated and inline logic for aggregation, filtering, and utility functions within loadDailyUsageData, loadSessionData, and loadMonthlyUsageData with calls to the new helper functions from data-aggregation.ts.
Using Gemini Code Assist

The 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 in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist is currently in preview and 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 to provide feedback.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configureGemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

This comment was marked as outdated.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new data-aggregation.ts module, refactoring common data aggregation logic out of data-loader.ts, improving readability and maintainability. The new helper functions are well-defined and effectively used. The review suggests enhancing clarity and type safety related to the calculateTotals return type, the filterByDateRange date format, and the spreading of the totals object in loadDailyUsageData and loadSessionData.

entries: T[],
getUsage: (entry: T) => UsageData['message']['usage'],
getCost: (entry: T) => number,
): TokenStats & { totalCost: number } {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The return type TokenStats & { totalCost: number } suggests cost (from TokenStats) and totalCost might differ. However, they're initialized and accumulated identically, creating redundancy. Consider returning only one of these values to avoid confusion.

*/
export function filterByDateRange<T>(
items: T[],
getDate: (item: T) => string,
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The filterByDateRange implementation assumes getDate returns a YYYY-MM-DD date string. Add a JSDoc comment to the getDate parameter specifying this expected format to ensure robustness.

Move all helper functions from data-aggregation.ts into data-loader.ts
since they are only used in that file. This improves code organization
by keeping related code together and reduces unnecessary file separation.

- Moved aggregateByModel, aggregateModelBreakdowns, createModelBreakdowns,
  calculateTotals, filterByDateRange, isDuplicateEntry, markAsProcessed,
  and extractUniqueModels functions into data-loader.ts
- Made all helper functions private (no export) since they are internal
- Deleted data-aggregation.ts file
- Removed import statement from data-loader.ts

This maintains the same functionality and test coverage while
simplifying the module structure.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/data-aggregation.ts (2)

32-46: Avoid repeated object-literal allocation & tighten typing

Inside the hot loop you allocate a fresh “zero” TokenStats object every time the model key is first seen.
Hoist this constant (or a factory) outside the loop, and/or use ??= to keep the existing object to avoid the extra allocation on every first encounter.
Additionally, usage is currently inferred as any, which triggers all the ts/no-unsafe- warnings reported by ESLint. Constrain the generic to something that contains a strongly-typed usage property to silence those warnings.

-const existing = modelAggregates.get(modelName) ?? {
-  inputTokens: 0,
-  ...
-};
+const defaultStats: TokenStats = {
+  inputTokens: 0,
+  outputTokens: 0,
+  cacheCreationTokens: 0,
+  cacheReadTokens: 0,
+  cost: 0,
+};
+const existing = modelAggregates.get(modelName) ?? defaultStats;

If you introduce a generic constraint:

export function aggregateByModel<T extends { /* whatever shape */ }>(...)

you’ll eliminate the unsafe-member-access noise.

🧰 Tools
🪛 ESLint

[error] 41-41: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 41-41: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 42-42: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 42-42: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 43-43: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 43-43: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 44-44: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 44-44: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


146-155: Date filtering is fragile for non-YYYY-MM-DD inputs

replace(/-/g, '') assumes the date string has only hyphens.
If getDate ever returns an ISO timestamp (2025-06-18T12:34:56Z) the compare logic silently breaks.

Consider normalising with new Date() and comparing epoch numbers, or slice the first 10 chars before stripping dashes:

-const dateStr = getDate(item).replace(/-/g, '');
+const dateStr = getDate(item).substring(0, 10).replace(/-/g, '');

This keeps the function tolerant to full ISO strings while preserving the cheap lexicographic compare trick.

src/data-loader.ts (1)

510-527: Duplicate-detection logic re-implemented instead of reused

In loadSessionData you’ve kept the old in-place deduplication (451-460) instead of using the new helpers, unlike the daily path above.

Unify both call sites for consistency and to avoid divergence:

-const uniqueHash = createUniqueHash(data);
-if (uniqueHash != null && processedHashes.has(uniqueHash)) …
-
-if (uniqueHash != null) { processedHashes.add(uniqueHash); }
+const uniqueHash = createUniqueHash(data);
+if (isDuplicateEntry(uniqueHash, processedHashes)) {
+  continue;
+}
+markAsProcessed(uniqueHash, processedHashes);

Reduces code and guarantees identical behaviour.

🧰 Tools
🪛 ESLint

[error] 513-513: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 513-513: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 523-523: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 523-523: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 292ede3 and 417e143.

📒 Files selected for processing (2)
  • src/data-aggregation.ts (1 hunks)
  • src/data-loader.ts (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/data-aggregation.ts (1)
src/data-loader.ts (2)
  • UsageData (46-46)
  • ModelBreakdown (57-57)
src/data-loader.ts (2)
src/data-aggregation.ts (7)
  • isDuplicateEntry (161-169)
  • markAsProcessed (174-181)
  • aggregateByModel (14-50)
  • createModelBreakdowns (89-98)
  • extractUniqueModels (186-191)
  • filterByDateRange (136-156)
  • aggregateModelBreakdowns (55-84)
src/calculate-cost.ts (1)
  • calculateTotals (18-37)
🪛 ESLint
src/data-aggregation.ts

[error] 29-29: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 41-41: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 41-41: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 42-42: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 42-42: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 43-43: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 43-43: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 44-44: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 44-44: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 62-62: Unsafe member access .modelName on an error typed value.

(ts/no-unsafe-member-access)


[error] 66-66: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)


[error] 66-66: Unsafe member access .modelName on an error typed value.

(ts/no-unsafe-member-access)


[error] 74-74: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)


[error] 74-74: Unsafe member access .modelName on an error typed value.

(ts/no-unsafe-member-access)


[error] 75-75: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 75-75: Unsafe member access .inputTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 76-76: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 76-76: Unsafe member access .outputTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 77-77: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 77-77: Unsafe member access .cacheCreationTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 78-78: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 78-78: Unsafe member access .cacheReadTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 79-79: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 79-79: Unsafe member access .cost on an error typed value.

(ts/no-unsafe-member-access)


[error] 110-110: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 114-114: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 114-114: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 115-115: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 115-115: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 116-116: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 116-116: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 117-117: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 117-117: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)

src/data-loader.ts

[error] 355-355: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 355-355: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 365-365: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 365-365: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 513-513: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 513-513: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 523-523: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 523-523: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 564-564: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 564-564: Unsafe member access .modelBreakdowns on an error typed value.

(ts/no-unsafe-member-access)

🔇 Additional comments (3)
src/data-loader.ts (3)

318-325: Nice reuse of the new dedup helpers

Switching to isDuplicateEntry / markAsProcessed makes the intent crystal-clear.


380-385: Elegant date-range filtering

Replacing the inline logic with filterByDateRange improves readability and removes duplication.


564-569: 👍 Aggregating monthly model data via aggregateModelBreakdowns

The flat-map + helper combo is concise and avoids an O(n²) merge loop.

🧰 Tools
🪛 ESLint

[error] 564-564: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 564-564: Unsafe member access .modelBreakdowns on an error typed value.

(ts/no-unsafe-member-access)

Comment on lines 107 to 120
): TokenStats & { totalCost: number } {
return entries.reduce(
(acc, entry) => {
const usage = getUsage(entry);
const cost = getCost(entry);

return {
inputTokens: acc.inputTokens + (usage.input_tokens ?? 0),
outputTokens: acc.outputTokens + (usage.output_tokens ?? 0),
cacheCreationTokens: acc.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
cacheReadTokens: acc.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
cost: acc.cost + cost,
totalCost: acc.totalCost + cost,
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

cost and totalCost are the same – drop one

calculateTotals accumulates both cost and totalCost, but they end up holding identical values.
This duplication is confusing and leaks an unused cost field into every caller (e.g. DailyUsage doesn’t define it).

- cost: acc.cost + cost,
- totalCost: acc.totalCost + cost,
+ totalCost: acc.totalCost + cost,

and remove cost from the accumulator’s seed & return type:

-export function calculateTotals<…>(): TokenStats & { totalCost: number }
+export function calculateTotals<…>(): Omit<TokenStats, 'cost'> & { totalCost: number }

This keeps the public API minimal and avoids accidental misuse.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
): TokenStats & { totalCost: number } {
return entries.reduce(
(acc, entry) => {
const usage = getUsage(entry);
const cost = getCost(entry);
return {
inputTokens: acc.inputTokens + (usage.input_tokens ?? 0),
outputTokens: acc.outputTokens + (usage.output_tokens ?? 0),
cacheCreationTokens: acc.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
cacheReadTokens: acc.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
cost: acc.cost + cost,
totalCost: acc.totalCost + cost,
};
export function calculateTotals<>(): Omit<TokenStats, 'cost'> & { totalCost: number } {
return entries.reduce(
(acc, entry) => {
const usage = getUsage(entry);
const cost = getCost(entry);
return {
inputTokens: acc.inputTokens + (usage.input_tokens ?? 0),
outputTokens: acc.outputTokens + (usage.output_tokens ?? 0),
cacheCreationTokens: acc.cacheCreationTokens + (usage.cache_creation_input_tokens ?? 0),
cacheReadTokens: acc.cacheReadTokens + (usage.cache_read_input_tokens ?? 0),
totalCost: acc.totalCost + cost,
};
},
// …your initial seed here, minus the `cost` field…
);
}
🧰 Tools
🪛 ESLint

[error] 110-110: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 114-114: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 114-114: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 115-115: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 115-115: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 116-116: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 116-116: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 117-117: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 117-117: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)

🤖 Prompt for AI Agents
In src/data-aggregation.ts around lines 107 to 120, the accumulator object in
the reduce function includes both 'cost' and 'totalCost' fields which hold
identical values, causing unnecessary duplication and confusion. Remove the
'cost' field entirely from the accumulator's initial seed, the reduce return
object, and the function's return type so that only 'totalCost' remains. This
will simplify the API and prevent leaking an unused 'cost' property to callers.

Comment on lines +352 to 370
const modelAggregates = aggregateByModel(
entries,
entry => entry.model,
entry => entry.data.message.usage,
entry => entry.cost,
);

// Create model breakdowns
const modelBreakdowns: ModelBreakdown[] = Array.from(modelAggregates.entries())
.map(([modelName, stats]) => ({
modelName,
...stats,
}))
.sort((a, b) => b.cost - a.cost); // Sort by cost descending
const modelBreakdowns = createModelBreakdowns(modelAggregates);

// Calculate totals
const totals = entries.reduce(
(acc, entry) => ({
inputTokens:
acc.inputTokens + (entry.data.message.usage.input_tokens ?? 0),
outputTokens:
acc.outputTokens + (entry.data.message.usage.output_tokens ?? 0),
cacheCreationTokens:
acc.cacheCreationTokens
+ (entry.data.message.usage.cache_creation_input_tokens ?? 0),
cacheReadTokens:
acc.cacheReadTokens
+ (entry.data.message.usage.cache_read_input_tokens ?? 0),
totalCost: acc.totalCost + entry.cost,
}),
{
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalCost: 0,
},
const totals = calculateTotals(
entries,
entry => entry.data.message.usage,
entry => entry.cost,
);

const modelsUsed = [...new Set(entries.map(e => e.model).filter((m): m is string => m != null && m !== '<synthetic>'))];
const modelsUsed = extractUniqueModels(entries, e => e.model);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

totals carries an unused cost property

Because calculateTotals still returns cost, the spread here injects an extra field not defined in DailyUsageSchema.
Either drop cost from calculateTotals (preferred – see earlier comment) or destructure explicitly:

-...totals,
+const { cost: _unused, ...restTotals } = totals;
+...restTotals,

Eliminates dead data and keeps runtime objects lean.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ESLint

[error] 355-355: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 355-355: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 365-365: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 365-365: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)

🤖 Prompt for AI Agents
In src/data-loader.ts between lines 352 and 370, the object returned by
calculateTotals includes an unused cost property that is not part of
DailyUsageSchema. To fix this, modify the calculateTotals function to exclude
the cost property from its return value, ensuring only the necessary fields are
returned. This will prevent injecting extra unused fields and keep the runtime
objects lean and aligned with the schema.

@ryoppippi ryoppippi requested a review from Copilot June 12, 2025 12:00

This comment was marked as outdated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/data-loader.ts (1)

199-227: Duplicate: calculateTotals still leaks an extra cost field

Previous review already pointed out that the returned object should not expose cost because it is not part of DailyUsageSchema / SessionUsageSchema.
Keeping it around still injects dead data when you later spread ...totals, and TypeScript won’t catch this because the type alias explicitly contains it.

-function calculateTotals<T>(
-  ...
-): TokenStats & { totalCost: number } {
+function calculateTotals<T>(
+  ...
+): Omit<TokenStats, 'cost'> & { totalCost: number } {
 ...
-        cost: acc.cost + cost,
         totalCost: acc.totalCost + cost,
 ...
-      cost: 0,
       totalCost: 0,
 }

After this change, remember to adjust the call sites (loadDailyUsageData, loadSessionData) to remove the extra property if you rely on structural typing.

🧰 Tools
🪛 ESLint

[error] 206-206: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 210-210: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 210-210: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 211-211: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 211-211: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 212-212: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 212-212: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 213-213: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 213-213: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 417e143 and 4a8d6762d2fb41b95cc683981751b722e18d05b2.

📒 Files selected for processing (1)
  • src/data-loader.ts (9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/data-loader.ts (1)
src/calculate-cost.ts (1)
  • calculateTotals (18-37)
🪛 ESLint
src/data-loader.ts

[error] 130-130: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 136-136: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 136-136: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 137-137: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 137-137: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 138-138: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 138-138: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 139-139: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 139-139: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 164-164: Unsafe member access .modelName on an error typed value.

(ts/no-unsafe-member-access)


[error] 168-168: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)


[error] 168-168: Unsafe member access .modelName on an error typed value.

(ts/no-unsafe-member-access)


[error] 170-170: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)


[error] 170-170: Unsafe member access .modelName on an error typed value.

(ts/no-unsafe-member-access)


[error] 171-171: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 171-171: Unsafe member access .inputTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 172-172: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 172-172: Unsafe member access .outputTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 173-173: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 173-173: Unsafe member access .cacheCreationTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 174-174: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 174-174: Unsafe member access .cacheReadTokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 175-175: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 175-175: Unsafe member access .cost on an error typed value.

(ts/no-unsafe-member-access)


[error] 206-206: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 210-210: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 210-210: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 211-211: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 211-211: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 212-212: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 212-212: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 213-213: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 213-213: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 537-537: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 537-537: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 547-547: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 547-547: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 693-693: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 693-693: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 703-703: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 703-703: Unsafe member access .message on an error typed value.

(ts/no-unsafe-member-access)


[error] 744-744: Unsafe return of a value of type error.

(ts/no-unsafe-return)


[error] 744-744: Unsafe member access .modelBreakdowns on an error typed value.

(ts/no-unsafe-member-access)

🔇 Additional comments (2)
src/data-loader.ts (2)

105-145: Helper extraction greatly improves readability 👍

Pulling the model aggregation loop into aggregateByModel removes a lot of duplicated map-handling code from the three loaders and makes the intent crystal-clear. Nice separation of concerns and correct handling of the synthetic model.

🧰 Tools
🪛 ESLint

[error] 130-130: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 136-136: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 136-136: Unsafe member access .input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 137-137: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 137-137: Unsafe member access .output_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 138-138: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 138-138: Unsafe member access .cache_creation_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


[error] 139-139: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 139-139: Unsafe member access .cache_read_input_tokens on an error typed value.

(ts/no-unsafe-member-access)


563-567: Date-range filter correctly applied – good touch

Filtering after aggregation keeps the critical path lean and avoids unnecessary work when large files are involved. Implementation is straightforward and correct.

Comment on lines +257 to +277
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Entries lacking IDs silently bypass deduplication

isDuplicateEntry returns false when either message ID or request ID is missing, so such entries are never tracked in processedHashes.
If a client repeatedly logs the same ID-less record, you’ll count it multiple times.

Consider a fallback—in the absence of a unique hash, derive a hash from timestamp + model + tokens (or simply always insert a placeholder string) so that duplicates are still suppressed.

🤖 Prompt for AI Agents
In src/data-loader.ts around lines 257 to 277, the isDuplicateEntry function
returns false when uniqueHash is null, causing entries without IDs to bypass
deduplication. To fix this, modify the logic to generate a fallback hash when
uniqueHash is null, for example by hashing a combination of timestamp, model,
and tokens or using a consistent placeholder string. Update both
isDuplicateEntry and markAsProcessed to use this fallback hash so that entries
lacking IDs are still tracked and duplicates suppressed.

- Fix TypeScript unsafe type issues in aggregation functions by improving
  generic constraints and hoisting defaultStats objects to avoid repeated
  allocation
- Improve date filtering robustness by taking first 10 chars before removing
  hyphens to handle ISO timestamp formats properly
- Unify duplicate detection logic in loadSessionData to use isDuplicateEntry
  and markAsProcessed helper functions consistently
- Remove .ts extensions from import statements following project conventions

Addresses feedback from PR #65 reviewers (CodeRabbit, Copilot, Gemini).
All lint and type checks now pass.
@ryoppippi ryoppippi force-pushed the refactor-data-loader branch from 4a8d676 to fbb507c Compare June 12, 2025 18:56
@ryoppippi ryoppippi requested a review from Copilot June 12, 2025 18:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors data aggregation logic by consolidating helper functions into data-loader.ts, improving maintainability, type safety, and performance. It also updates import statements for module resolution consistency.

  • Extracted common data aggregation helpers and duplicate detection logic into consolidated functions.
  • Enhanced type safety with improved generic constraints and updated import paths.
  • Fixed linting and module resolution issues in several files.

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/shared-args.internal.ts Updated the import for getDefaultClaudePath to include an explicit .ts extension.
src/mcp.ts Changed import statements for LoadOptions and data-loader to include explicit .ts.
src/calculate-cost.ts Updated import for usage types from data-loader to include the .ts extension.
Comments suppressed due to low confidence (4)

src/shared-args.internal.ts:4

  • The import now explicitly includes the '.ts' extension. Confirm that using explicit extensions aligns with the project’s module resolution strategy and is consistent with other file imports.
import { getDefaultClaudePath } from './data-loader.ts';

src/mcp.ts:1

  • Using the explicit .ts extension in the import statement may be against the project convention if extensions are meant to be omitted. Verify for consistency with the project’s import style.
import type { LoadOptions } from './data-loader.ts';

src/mcp.ts:11

  • Ensure consistent use of the explicit '.ts' extension in inline import paths across the file in line with the project standards.
} from './data-loader.ts';

src/calculate-cost.ts:1

  • This import now includes the '.ts' extension explicitly. Double-check that this approach is deliberate and consistent with the team’s conventions for module imports.
import type { DailyUsage, MonthlyUsage, SessionUsage } from './data-loader.ts';

@ryoppippi ryoppippi merged commit 5efa904 into main Jun 13, 2025
13 of 21 checks passed
@ryoppippi ryoppippi deleted the refactor-data-loader branch June 13, 2025 21:14
ryoppippi added a commit that referenced this pull request Jun 22, 2025
- Fix TypeScript unsafe type issues in aggregation functions by improving
  generic constraints and hoisting defaultStats objects to avoid repeated
  allocation
- Improve date filtering robustness by taking first 10 chars before removing
  hyphens to handle ISO timestamp formats properly
- Unify duplicate detection logic in loadSessionData to use isDuplicateEntry
  and markAsProcessed helper functions consistently
- Remove .ts extensions from import statements following project conventions

Addresses feedback from PR #65 reviewers (CodeRabbit, Copilot, Gemini).
All lint and type checks now pass.
ryoppippi added a commit that referenced this pull request Jun 22, 2025
feat: add data-aggregation module with helper functions
@coderabbitai coderabbitai bot mentioned this pull request Nov 15, 2025
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants