Skip to content

Conversation

@akshayutture-augment
Copy link

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Test 1

Summary by CodeRabbit

  • New Features
    • Added configurable anonymous device limit feature to control the maximum number of anonymous devices allowed in the system
    • Implemented device limit enforcement that prevents new anonymous device registration when the configured limit is reached
    • Added automatic device expiration tracking with a 30-day window for anonymous device lifecycle management

Replicated from ai-code-review-evaluation/grafana-coderabbit#1

* Anonymous: Add device limiter

* break auth if limit reached

* fix typo

* refactored const to make it clearer with expiration

* anon device limit for config

---------

Co-authored-by: Eric Leijonmarck <[email protected]>
@akshayutture-augment
Copy link
Author

@coderabbitai review

@akshayutture-augment
Copy link
Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Walkthrough

The pull request introduces an anonymous device limit feature across Grafana's configuration and backend services. A new configurable AnonymousDeviceLimit setting propagates through frontend configuration, frontend settings DTOs, and the anonymous device store, where enforcement logic prevents creating new devices when the limit is reached via a new ErrDeviceLimitReached error.

Changes

Cohort / File(s) Summary
Frontend Configuration
packages/grafana-data/src/types/config.ts, packages/grafana-runtime/src/config.ts
Added anonymousDeviceLimit: number | undefined property to GrafanaConfig and GrafanaBootConfig interfaces for frontend configuration exposure.
Frontend Settings Data Transfer
pkg/api/dtos/frontend_settings.go, pkg/api/frontendsettings.go
Added AnonymousDeviceLimit int64 field to FrontendSettingsDTO struct and populated it from configuration in the frontend settings builder.
Core Settings Configuration
pkg/setting/setting.go
Added AnonymousDeviceLimit int64 public field to Cfg struct and reads device_limit from auth.anonymous configuration section.
Anonymous Device Store & Limits
pkg/services/anonymous/anonimpl/anonstore/database.go, pkg/services/anonymous/anonimpl/anonstore/database_test.go
Updated ProvideAnonDBStore function signature to accept deviceLimit parameter; added deviceLimit field to AnonDBStore; added anonymousDeviceExpiration constant (30 days) and ErrDeviceLimitReached error; introduced updateDevice helper and conditional logic in CreateOrUpdateDevice to enforce limits and return error when exceeded. Updated test call sites and added TestIntegrationBeyondDeviceLimit test.
Anonymous Service Integration
pkg/services/anonymous/anonimpl/impl.go, pkg/services/anonymous/anonimpl/impl_test.go, pkg/services/anonymous/anonimpl/client.go
Changed ProvideAnonymousDeviceService to accept sqlStore instead of anonStore parameter and creates store via ProvideAnonDBStore; added error handling for ErrDeviceLimitReached in TagDevice call path; removed timeout-based goroutine in favor of direct TagDevice call with incoming context.
Anonymous Service API
pkg/services/anonymous/anonimpl/api/api.go
Replaced hard-coded thirtDays constant with anonymousDeviceExpiration constant in ListDevices method.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Service as TagDevice<br/>(Service)
    participant Store as AnonDBStore
    participant DB as Database
    
    Client->>Service: TagDevice(ctx, deviceRef)
    Service->>Store: CreateOrUpdateDevice(ctx, deviceRef)
    
    alt Device Limit Enforced (limit > 0)
        Store->>DB: Query device count within<br/>expiration window
        DB-->>Store: count result
        
        alt Count >= Limit
            Store->>DB: UPDATE device (updateDevice)
            DB-->>Store: rows affected
            
            alt No rows affected
                Store-->>Service: ErrDeviceLimitReached ❌
                Service-->>Client: Error returned
            else Rows updated
                Store-->>Service: Success ✓
                Service-->>Client: Device updated
            end
        else Count < Limit
            Store->>DB: INSERT new device
            DB-->>Store: device created
            Store-->>Service: Success ✓
            Service-->>Client: New device created
        end
    else No Limit (limit ≤ 0)
        Store->>DB: INSERT or UPDATE device
        DB-->>Store: Success
        Store-->>Service: Success ✓
        Service-->>Client: Device created/updated
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Areas requiring extra attention:

  • Device limit enforcement logic (database.go): The conditional branching in CreateOrUpdateDevice that checks device count and delegates to updateDevice helper requires careful verification of SQL query correctness and edge cases around expiration window boundaries.
  • Dependency injection changes (impl.go, impl_test.go): The shift from injected anonStore parameter to creating it from sqlStore with device limit configuration needs validation that all initialization paths pass correct device limits.
  • Error propagation (client.go): The new early return for ErrDeviceLimitReached changes control flow; verify logging and error handling remain consistent with surrounding error paths.
  • Test coverage (database_test.go, impl_test.go): Verify the new TestIntegrationBeyondDeviceLimit test adequately covers the device limit boundary conditions and that existing test updates properly reflect the new function signatures.

Poem

🐰 Hopping through configs, we've set a new gate,
With device limits bouncing, no more running late,
The store counts and checks with the expiration timer,
Each new device tagged (or caught by the limiter),
A tidy solution to rein in the horde! 🎯

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a configurable device limit to the anonymous device feature across multiple files and services.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch implement-device-limits

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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 (5)
pkg/services/anonymous/anonimpl/anonstore/database.go (2)

16-16: Consider exporting the constant for consistency.

The anonymousDeviceExpiration constant is also duplicated in pkg/services/anonymous/anonimpl/api/api.go (line 18). Consider exporting this constant from a single location to ensure consistency across the codebase.

For example, export it from this file:

-const anonymousDeviceExpiration = 30 * 24 * time.Hour
+const AnonymousDeviceExpiration = 30 * 24 * time.Hour

Then import and use it in api.go instead of redefining it.


18-18: Enhance error message with more context.

The error message could be more helpful by including the device limit value or suggesting what users can do.

Consider creating a structured error type:

type ErrDeviceLimitReached struct {
    Limit int64
}

func (e *ErrDeviceLimitReached) Error() string {
    return fmt.Sprintf("anonymous device limit of %d reached", e.Limit)
}

This would allow callers to extract the limit value if needed for user-facing messages.

pkg/services/anonymous/anonimpl/impl_test.go (2)

116-117: Note the reduced test flexibility.

The test now passes the raw db.DB to the service, which internally creates its own store. While acceptable for integration tests, this reduces the ability to inject mock stores for unit testing. Consider whether unit tests with mocked stores would be valuable.


124-124: Consider using the public API instead of accessing internal fields.

The test directly accesses anonService.anonStore.ListDevices(), but the service already exposes a public ListDevices() method (line 154 of impl.go). Using the public API would decouple the test from internal implementation details.

Apply this diff to use the public API:

-			devices, err := anonService.anonStore.ListDevices(context.Background(), nil, nil)
+			devices, err := anonService.ListDevices(context.Background(), nil, nil)
pkg/services/anonymous/anonimpl/impl.go (1)

36-39: Constructor signature changed from dependency injection to internal construction.

The service now receives sqlStore db.DB instead of anonStore anonstore.AnonStore, shifting from dependency injection to internal store construction. While this provides better control over store configuration (e.g., passing the device limit), it reduces testability and flexibility. Consider whether this trade-off aligns with your testing strategy.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed86583 and 3647ba7.

📒 Files selected for processing (11)
  • packages/grafana-data/src/types/config.ts (1 hunks)
  • packages/grafana-runtime/src/config.ts (1 hunks)
  • pkg/api/dtos/frontend_settings.go (1 hunks)
  • pkg/api/frontendsettings.go (1 hunks)
  • pkg/services/anonymous/anonimpl/anonstore/database.go (3 hunks)
  • pkg/services/anonymous/anonimpl/anonstore/database_test.go (2 hunks)
  • pkg/services/anonymous/anonimpl/api/api.go (2 hunks)
  • pkg/services/anonymous/anonimpl/client.go (2 hunks)
  • pkg/services/anonymous/anonimpl/impl.go (4 hunks)
  • pkg/services/anonymous/anonimpl/impl_test.go (2 hunks)
  • pkg/setting/setting.go (2 hunks)
🔇 Additional comments (12)
pkg/services/anonymous/anonimpl/client.go (1)

44-50: Verify authentication behavior when device limit is reached.

When ErrDeviceLimitReached is returned, the authentication will fail. This means that even existing anonymous users may be unable to authenticate if the device limit has been reached by other users. Consider whether the intended behavior is to:

  • Block all anonymous access when the limit is reached (current implementation)
  • Allow existing devices to continue but block new devices
  • Provide a more graceful degradation

If the current behavior is intentional, consider adding user-friendly error messaging that explains why authentication failed.

pkg/services/anonymous/anonimpl/api/api.go (1)

18-18: LGTM - Constant renamed for consistency.

The constant name now matches the one defined in database.go, improving consistency. As noted in the database.go review, consider exporting and sharing a single constant definition to avoid duplication.

packages/grafana-runtime/src/config.ts (1)

97-97: LGTM - Frontend config property added.

The new anonymousDeviceLimit property is correctly added with an appropriate default value. The number | undefined type aligns well with the backend's int64 and the optional nature of this configuration.

packages/grafana-data/src/types/config.ts (1)

200-200: LGTM - Type definition added correctly.

The field is properly typed as number | undefined, matching the optional nature of the configuration and aligning with the implementation in packages/grafana-runtime/src/config.ts.

pkg/api/frontendsettings.go (1)

198-198: LGTM - Frontend settings population added.

The AnonymousDeviceLimit is correctly populated from the configuration. This change is consistent with how other configuration values are exposed to the frontend.

pkg/api/dtos/frontend_settings.go (1)

195-195: LGTM - DTO field added correctly.

The AnonymousDeviceLimit field is properly defined with the correct type (int64) and JSON tag. The placement next to AnonymousEnabled is logical.

pkg/services/anonymous/anonimpl/anonstore/database_test.go (2)

16-16: LGTM - Existing tests updated correctly.

The existing test functions are properly updated to pass deviceLimit=0 to maintain their original behavior (no limit enforcement).

Also applies to: 74-74


51-70: Test coverage added for device limit feature.

The new test TestIntegrationBeyondDeviceLimit correctly exercises the device limit enforcement:

  1. Creates a device successfully when under the limit
  2. Attempts to create a second device and expects ErrDeviceLimitReached

However, note that this test may be sensitive to the race condition and time window logic issues identified in the database.go review. If those issues are addressed, this test may need adjustment.

Consider adding additional test cases:

  • Test updating an existing device when at the limit (should succeed per the current logic)
  • Test with devices outside the expiration window (should not count toward limit)
  • Test concurrent device creation to verify race condition behavior
pkg/setting/setting.go (2)

1650-1655: LGTM: Configuration reading is well-structured.

The refactoring to reuse anonSection improves efficiency by avoiding repeated section lookups. The configuration key naming and default value handling are consistent with the surrounding code.


375-375: AnonymousDeviceLimit = 0 correctly means "unlimited devices."

The code at pkg/services/anonymous/anonimpl/anonstore/database.go:109 confirms the semantics: the device limit is only enforced when deviceLimit > 0. When the default value is 0, the limit-checking block is skipped entirely, allowing new devices to be registered without restriction. This behavior is intentional and correct.

pkg/services/anonymous/anonimpl/impl.go (2)

43-43: LGTM: Device limit configuration is properly wired.

The store is correctly initialized with the device limit from configuration, ensuring consistent enforcement across the service.


144-148: Behavior change verified but partially mitigated: Error propagation incomplete in caller.

The change is confirmed—TagDevice now returns errors from tagDeviceUI instead of swallowing them. However, verification shows:

  • client.go line 44 already checks if err := a.anonDeviceService.TagDevice(...); err != nil
  • Only ErrDeviceLimitReached is returned to upstream callers (lines 45-46)
  • Other errors are logged at line 49 but not returned—the function still returns success (line 52)

The original goroutine approach was also removed and replaced with synchronous execution, which is a separate change not covered in the original review.

Recommendation: The error is no longer completely swallowed at the TagDevice level, but incomplete error handling persists in client.go. Consider whether non-limit errors should propagate upstream or remain logged and suppressed.

Comment on lines +72 to +103
// updateDevice updates a device if it exists and has been updated between the given times.
func (s *AnonDBStore) updateDevice(ctx context.Context, device *Device) error {
const query = `UPDATE anon_device SET
client_ip = ?,
user_agent = ?,
updated_at = ?
WHERE device_id = ? AND updated_at BETWEEN ? AND ?`

args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID,
device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), device.UpdatedAt.UTC().Add(time.Minute),
}
err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
args = append([]interface{}{query}, args...)
result, err := dbSession.Exec(args...)
if err != nil {
return err
}

rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}

if rowsAffected == 0 {
return ErrDeviceLimitReached
}

return nil
})

return err
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify the time window logic and error semantics.

The updateDevice function has several concerns:

  1. Ambiguous error: Returns ErrDeviceLimitReached when no rows are affected (line 95-96), but zero rows could mean:

    • Device doesn't exist in the database
    • Device exists but was last updated outside the time window (older than 30 days or in the future)

    This makes the error potentially misleading.

  2. Unusual upper time bound: The WHERE clause checks updated_at BETWEEN (now - 30 days) AND (now + 1 minute) (line 81). The upper bound of now + 1 minute seems intended to handle clock skew, but:

    • It's inconsistent with the device expiration logic
    • Consider using just updated_at >= ? with a single lower bound
    • Or document why the upper bound is necessary

Consider refactoring to make the intent clearer:

 func (s *AnonDBStore) updateDevice(ctx context.Context, device *Device) error {
 	const query = `UPDATE anon_device SET
 client_ip = ?,
 user_agent = ?,
 updated_at = ?
-WHERE device_id = ? AND updated_at BETWEEN ? AND ?`
+WHERE device_id = ? AND updated_at >= ?`
 
-	args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID,
-		device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), device.UpdatedAt.UTC().Add(time.Minute),
-	}
+	args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID,
+		device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration),
+	}

And potentially return a different error or add a comment explaining that ErrDeviceLimitReached in this context means "device not found or expired, cannot update."

🤖 Prompt for AI Agents
In pkg/services/anonymous/anonimpl/anonstore/database.go around lines 72 to 103,
the updateDevice function uses a BETWEEN window with a future upper bound and
returns ErrDeviceLimitReached when no rows are affected, which is ambiguous;
change the WHERE clause to use a single lower bound (e.g. updated_at >= ?) to
avoid the odd now+1min upper bound (or if you must keep the upper bound add a
clear comment explaining clock-skew handling), update the args to pass only the
lower bound (device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration)), and
replace or wrap the ErrDeviceLimitReached return with a clearer error (e.g.
ErrDeviceNotFoundOrExpired) so callers can distinguish "no matching/expired
device" from hitting a device limit; also add a brief comment above the query
describing the exact semantics (why the bound(s) are used) and ensure
tests/assertions reflect the new error semantics.

Comment on lines +108 to +118
// if device limit is reached, only update devices
if s.deviceLimit > 0 {
count, err := s.CountDevices(ctx, time.Now().UTC().Add(-anonymousDeviceExpiration), time.Now().UTC().Add(time.Minute))
if err != nil {
return err
}

if count >= s.deviceLimit {
return s.updateDevice(ctx, device)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition in device limit enforcement.

The device limit check has a race condition:

  1. Line 110: CountDevices is called to check the current count
  2. Line 115-116: If the limit is reached, updateDevice is called
  3. Lines 120-155: Otherwise, a new device is created

Between the count check and the insert/update, another concurrent request could create a device, causing the actual count to exceed the limit.

This race condition could allow the device limit to be exceeded under concurrent load. Consider one of these approaches:

  1. Use database constraints: Add a mechanism to enforce the limit at the database level (though this is complex for row count limits)

  2. Use locking: Acquire a lock before counting and hold it through the insert/update:

// Pseudo-code
s.lock.Lock()
defer s.lock.Unlock()
count, err := s.CountDevices(...)
if count >= s.deviceLimit {
    return s.updateDevice(ctx, device)
}
// proceed with insert
  1. Accept eventual consistency: Document that the limit is approximate and may be briefly exceeded under high concurrency, which might be acceptable for this use case

  2. Retry logic: After insert, check if count exceeded and remove the most recent device

Which approach is best depends on how strict the limit enforcement needs to be.

🤖 Prompt for AI Agents
In pkg/services/anonymous/anonimpl/anonstore/database.go around lines 108 to
118, the device limit check has a race where CountDevices and the subsequent
insert/update are not atomic, allowing concurrent requests to exceed the limit;
fix by serializing the critical section: add a mutex (or a named lock) on the
store struct, acquire the lock before calling CountDevices and hold it through
the decision and the eventual insert or update, defer unlock immediately after
acquiring, ensure the lock acquisition respects context/cancellation (or times
out) and return an error if it cannot be acquired, and keep the existing
updateDevice/insert logic inside that locked section so the count and
modification are atomic from the process perspective.

@akshayutture-augment
Copy link
Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

3 participants