Skip to content

Feat/me usage#282

Merged
ding113 merged 7 commits intodevfrom
feat/me-usage
Dec 6, 2025
Merged

Feat/me usage#282
ding113 merged 7 commits intodevfrom
feat/me-usage

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Dec 6, 2025

Summary

This PR introduces a self-service "My Usage" portal that allows non-admin users to view their own quotas, usage statistics, and request logs without accessing the admin dashboard. The feature enhances user autonomy and reduces admin workload by providing transparent access to personal usage data.

Problem

Previously, users had no way to:

  • Check their own quota limits and current usage
  • View their request history and costs
  • Monitor their token consumption
  • Track which models they've been using

This lack of visibility forced users to contact administrators for basic usage information, creating unnecessary overhead and reducing transparency.

Solution

Implemented a dedicated /my-usage page with three main sections:

1. Quota Display

  • Shows limits and current usage for 5h/daily/weekly/monthly/total quotas
  • Displays concurrent session limits and current usage
  • Separates key-level and user-level quotas with visual progress bars
  • Respects both fixed and rolling daily reset modes

2. Today's Statistics (Auto-refresh)

  • Real-time overview of today's usage (calls, tokens, cost)
  • Model-by-model breakdown with billing details
  • Auto-refresh every 30 seconds
  • Respects system-wide billing model source setting

3. Usage Logs

  • Paginated request history with filters (date range, model, status code)
  • Shows input/output tokens, cost, duration, and endpoint
  • Displays model redirects when applicable
  • Supports multi-language date formatting

Changes

New Files

  • src/actions/my-usage.ts - Server actions for fetching quota, stats, and logs
  • src/app/[locale]/my-usage/page.tsx - Main usage page component
  • src/app/[locale]/my-usage/_components/ - UI components (header, quota cards, today stats, logs table)
  • messages/*/myUsage.json - i18n translations for en/zh-CN/zh-TW/ja/ru

Modified Files

  • src/app/[locale]/dashboard/layout.tsx - Redirect non-admin users without canLoginWebUi to /my-usage
  • src/app/[locale]/login/page.tsx - Support redirectTo from login response
  • messages/*/index.ts - Register new myUsage namespace for all locales

Key Technical Details

  • Uses existing rate-limit and session tracking infrastructure
  • Leverages getSession({ allowReadOnlyAccess: true }) for read-only access paths
  • Queries aggregate user-level costs across all keys for user quota tracking
  • Timezone-aware queries using getEnvConfig().TZ for "today" stats
  • Respects billingModelSource system setting for displaying billing models

Testing

  • Manual testing performed
  • Verified quota display for keys with/without limits
  • Tested usage logs filtering and pagination
  • Confirmed auto-refresh functionality for today's stats
  • Validated all five language translations (en/zh-CN/zh-TW/ja/ru)
  • Tested redirect behavior for non-admin users
  • Verified read-only access paths work correctly
  • No breaking changes to existing admin dashboard

Screenshots

UI screenshots showing quota cards, today's statistics, and usage logs table

Related Changes

This PR builds on recent auth infrastructure improvements:

  • Enhanced login flow and permissions management (9377efc)
  • Added read-only access paths for key validation (76ef544)
  • Streamlined filter handling and JSON structure (6144e98)

Migration Notes

No database migrations required. Uses existing tables (message_request, keys, users).

ding113 and others added 5 commits December 5, 2025 01:24
- Added a new function to determine the redirect target after login based on user role and permissions.
- Updated the `validateKey` function to allow read-only access for users without Web UI login permissions.
- Modified the dashboard layout to redirect users without admin access to their usage page.
- Introduced new methods to retrieve total usage and distinct models for a given API key.

This update improves user experience by ensuring appropriate access control and navigation based on user roles.
- Introduced READ_ONLY_PATH_PATTERNS to allow specific paths to bypass the canLoginWebUi check.
- Updated the proxyHandler to check for read-only access, modifying the validateKey function call accordingly.

This change improves access control for users without Web UI login permissions, ensuring they can still access necessary information.
- Removed redundant nesting in myUsage JSON files for cleaner structure.
- Updated filter handling in the usage logs section to use a default value of "__all__" for model and status filters, enhancing user experience.
- Ensured consistent terminology across multiple language files for better localization support.

This refactor improves the maintainability of the code and enhances the user interface for filtering usage logs.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, 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 delivers a significant new feature: a 'My Usage' section for users. This section provides a clear overview of API key quotas and usage logs, enhancing user transparency and control over their consumption. The changes span across new UI components, server-side data fetching logic, and crucial updates to the authentication system to correctly route users based on their key's web UI access permissions. It also adjusts the default behavior for new API keys regarding web interface login capabilities.

Highlights

  • New 'My Usage' Feature: Introduced a dedicated 'My Usage' page where users can view their API key quotas, daily usage statistics, and detailed usage logs. This provides transparency and self-service capabilities for monitoring API consumption.
  • Internationalization Support: Added comprehensive internationalization (i18n) for the new 'My Usage' page, with translations provided for English, Japanese, Russian, Simplified Chinese, and Traditional Chinese.
  • Enhanced Authentication and Redirection: Updated the authentication flow to support read-only access for keys that do not have full dashboard privileges. Users logging in with such keys will now be automatically redirected to the new 'My Usage' page instead of the dashboard.
  • Server Actions for Usage Data: Implemented new server actions (src/actions/my-usage.ts) to efficiently fetch and process user-specific quota, today's usage, and historical log data, leveraging Drizzle ORM for database interactions.
  • Default Key Web UI Access Change: The default value for canLoginWebUi in the keys table has been changed from true to false, meaning new API keys will no longer have web UI login access by default.
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 by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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 pull request 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 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

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

@ding113 ding113 added size/L Large PR (< 1000 lines) enhancement New feature or request size/XL Extra Large PR (> 1000 lines) labels Dec 6, 2025
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 'My Usage' page, which is a great feature for end-users to monitor their quota and usage logs. The implementation is well-structured, with clear separation of concerns between server actions, components, and internationalization files. The access control logic, which redirects users based on their permissions (canLoginWebUi), is handled robustly in the middleware, API routes, and page layouts.

My review includes a few suggestions for improvement:

  • A high-severity note on a change to a default value in the database schema that could have a significant impact on new key creation.
  • A couple of medium-severity suggestions to improve performance and maintainability in the data-fetching logic.

Overall, this is a solid contribution. Addressing these points will make the implementation even more robust and scalable.

I am having trouble creating individual review comments. Click here to see my feedback.

src/drizzle/schema.ts (59)

high

The default value for canLoginWebUi has been changed from true to false. This is a significant behavioral change that will affect all newly created API keys, as they will no longer have access to the main dashboard by default and will be redirected to the "My Usage" page instead. While this seems intentional for the new feature, it's a breaking change for administrators who expect the previous default behavior. Please ensure this change is clearly communicated and that existing key creation workflows are updated to explicitly set canLoginWebUi: true if full dashboard access is desired for new keys.

src/actions/my-usage.ts (372-385)

medium

The getUserConcurrentSessions function fetches all keys for a user and then iterates through them, calling SessionTracker.getKeySessionCount for each one. This results in N+1 database/Redis queries (1 to get keys, N to get session counts), which can be inefficient if a user has a large number of keys.

Consider optimizing this by creating a batch-capable function in SessionTracker, similar to getProviderSessionCountBatch, that can fetch session counts for multiple keys in a more efficient way (e.g., using a single Redis pipeline).

src/repository/usage-logs.ts (80-92)

medium

The getDistinctModelsForKey function uses raw SQL with db.execute. While this works, it bypasses Drizzle's type safety and query building capabilities. For better maintainability and consistency with the rest of the repository's data access patterns, consider refactoring this to use the Drizzle query builder.

export async function getDistinctModelsForKey(keyString: string): Promise<string[]> {
  const modelColumn = sql<string>`coalesce(${messageRequest.originalModel}, ${messageRequest.model})`.as("model");

  const rows = await db
    .selectDistinct({ model: modelColumn })
    .from(messageRequest)
    .where(and(eq(messageRequest.key, keyString), isNull(messageRequest.deletedAt)))
    .orderBy(modelColumn);

  return rows
    .map((row) => row.model)
    .filter((model): model is string => !!model && model.trim().length > 0);
}

filters: MyUsageLogsFilters = {}
): Promise<ActionResult<MyUsageLogsResult>> {
try {
const session = await getSession({ allowReadOnlyAccess: true });
Copy link
Owner Author

Choose a reason for hiding this comment

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

High Type mismatch in usage log filters

Why this is a problem: The UsageLogFilters interface expects startTime and endTime as numbers (milliseconds), but the code is passing startDateLocal and endDateLocal as strings. This will cause the time filters to be silently ignored, and the query will return all logs instead of the filtered time range.

Suggested fix:

const usageFilters: UsageLogFilters = {
  keyId: session.key.id,
  startTime: filters.startDate ? new Date(`${filters.startDate}T00:00`).getTime() : undefined,
  endTime: filters.endDate ? new Date(`${filters.endDate}T23:59:59.999`).getTime() : undefined,
  model: filters.model,
  statusCode: filters.statusCode,
  page,
  pageSize,
};

return Number(row?.total ?? 0);
}

export async function getDistinctModelsForKey(keyString: string): Promise<string[]> {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Medium Potential SQL injection in raw SQL query

Why this is a problem: The getDistinctModelsForKey function uses raw SQL with string interpolation that could be vulnerable to SQL injection if the keyString parameter is not properly sanitized. While Drizzle's sql tagged template should handle parameterization, mixing it with direct variable references is error-prone.

Suggested fix:

export async function getDistinctModelsForKey(keyString: string): Promise<string[]> {
  const result = await db
    .selectDistinct({
      model: sql<string>`coalesce(${messageRequest.originalModel}, ${messageRequest.model})`,
    })
    .from(messageRequest)
    .where(
      and(
        eq(messageRequest.key, keyString),
        isNull(messageRequest.deletedAt),
        sql`coalesce(${messageRequest.originalModel}, ${messageRequest.model}) is not null`
      )
    )
    .orderBy(sql`coalesce(${messageRequest.originalModel}, ${messageRequest.model}) asc`);

  return result
    .map((row) => row.model)
    .filter((model): model is string => !!model && model.trim().length > 0);
}

}

async function getUserConcurrentSessions(userId: number): Promise<number> {
try {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Low Error handling silently returns 0 on failure

Why this is a problem: The getUserConcurrentSessions function catches all errors and returns 0, which could mask database connection issues or other failures. Callers won't know if the returned 0 means "no sessions" or "failed to check".

Suggested fix:

async function getUserConcurrentSessions(userId: number): Promise<number> {
  const keys = await db
    .select({ id: keysTable.id })
    .from(keysTable)
    .where(and(eq(keysTable.userId, userId), isNull(keysTable.deletedAt)));

  const counts = await Promise.all(keys.map((k) => SessionTracker.getKeySessionCount(k.id)));
  return counts.reduce((sum, value) => sum + value, 0);
}

Remove the try-catch and let errors propagate to the caller's error handler.

Copy link
Owner Author

@ding113 ding113 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 Summary

This PR adds a "My Usage" feature for non-admin users to view their quota and usage logs. The implementation is mostly solid but contains one critical type mismatch bug that will break time filtering functionality.

PR Size: L

  • Lines changed: 1529
  • Files changed: 25

Issues Found

Category Critical High Medium Low
Code Quality 0 1 0 1
Security 0 0 1 0
Dependencies 0 0 0 0
Documentation 0 0 0 0

Priority Actions

  1. Fix the type mismatch in getMyUsageLogs (High) - The time filter parameters use wrong field names (startDateLocal/endDateLocal instead of startTime/endTime), causing filters to be silently ignored. This is a functional bug.

  2. Refactor raw SQL query (Medium) - The getDistinctModelsForKey function uses db.execute with raw SQL which is error-prone. Should use Drizzle's query builder for consistency and safety.

  3. Remove unnecessary error swallowing (Low) - The getUserConcurrentSessions function catches all errors and returns 0, masking potential database issues.

Review Coverage

  • Code quality and correctness
  • Security (OWASP Top 10)
  • PR size assessment - Large
  • Dependency changes - No dependency changes
  • Documentation changes - Multiple i18n translation files added

Automated review by Claude AI

Fix TypeScript error where startDateLocal and endDateLocal properties
don't exist in UsageLogFilters interface. The interface expects
startTime and endTime as millisecond timestamps instead.

- Changed from startDateLocal/endDateLocal to startTime/endTime
- Convert date strings to millisecond timestamps using Date.getTime()
- Use proper time boundaries: 00:00:00 for start, 23:59:59.999 for end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size/L Large PR (< 1000 lines) size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant