Skip to content

Automated Test: implement-device-limits #326

Closed

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added configurable anonymous device limit setting to control maximum concurrent anonymous devices
    • System now enforces device limits during authentication with appropriate error messaging when limits are exceeded

* 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 <eric.leijonmarck@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a device limit feature for anonymous access in Grafana. The changes add a new anonymousDeviceLimit configuration property across frontend and backend layers, implement device limit enforcement in the anonymous device store, and update the authentication flow to return errors when the device limit is reached.

Changes

Cohort / File(s) Summary
Frontend Configuration Types
packages/grafana-data/src/types/config.ts, packages/grafana-runtime/src/config.ts
Added anonymousDeviceLimit: number | undefined property to GrafanaConfig and GrafanaBootConfig types to expose the device limit setting to the frontend.
Frontend Settings Serialization
pkg/api/dtos/frontend_settings.go, pkg/api/frontendsettings.go
Added AnonymousDeviceLimit field to FrontendSettingsDTO and populated it from configuration in the frontend settings endpoint.
Anonymous Device Store
pkg/services/anonymous/anonimpl/anonstore/database.go
Introduced deviceLimit field to AnonDBStore, added ErrDeviceLimitReached error variable, and implemented limit enforcement in CreateOrUpdateDevice via a new updateDevice method that checks device expiration windows.
Anonymous Device Service
pkg/services/anonymous/anonimpl/impl.go
Modified ProvideAnonymousDeviceService to accept sqlStore and construct AnonDBStore internally with the device limit; added error handling to return device limit errors from TagDevice.
Configuration & Settings
pkg/setting/setting.go
Added AnonymousDeviceLimit int64 field to Cfg struct and implemented loading from the auth.anonymous.device_limit configuration setting.
API & Client Updates
pkg/services/anonymous/anonimpl/api/api.go, pkg/services/anonymous/anonimpl/client.go
Replaced hardcoded 30-day constant with named anonymousDeviceExpiration in API; changed device tagging from async to sync in client with immediate error propagation for device limit violations.
Test Updates
pkg/services/anonymous/anonimpl/anonstore/database_test.go, pkg/services/anonymous/anonimpl/impl_test.go
Updated store initialization calls to include deviceLimit parameter; added TestIntegrationBeyondDeviceLimit to verify limit enforcement; updated service test references to use internal anonStore field.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Request
    participant AuthBroker as Auth Service
    participant AnonService as Anonymous Device Service
    participant AnonStore as Anonymous Device Store
    participant Database as Database
    
    Client->>AuthBroker: Authenticate
    AuthBroker->>AnonService: TagDevice
    AnonService->>AnonStore: CreateOrUpdateDevice
    
    alt Device count within limit or first device
        AnonStore->>Database: Upsert device record
        Database-->>AnonStore: Success
        AnonStore-->>AnonService: Device created/updated
        AnonService-->>AuthBroker: Success
        AuthBroker-->>Client: Identity returned
    else Device limit reached
        AnonStore->>AnonStore: Check device count vs limit
        AnonStore-->>AnonService: ErrDeviceLimitReached
        AnonService-->>AuthBroker: ErrDeviceLimitReached
        AuthBroker-->>Client: Error returned
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Through config layers and database deep,
A limit on devices we now keep,
When anonymous guests exceed the bound,
An error is returned, not a single sound,
A hoppy refactor that's simple and clean!

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: implement-device-limits' is partially related to the changeset, describing the feature being implemented but lacks specificity about what the change accomplishes. Consider a more descriptive title that explains the primary change, such as 'Add anonymous device limit enforcement' or 'Implement device limit configuration for anonymous access'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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

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

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval 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

🤖 Fix all issues with AI agents
In `@pkg/services/anonymous/anonimpl/anonstore/database.go`:
- Around line 108-118: The CountDevices check in database.go (using
s.CountDevices) and the subsequent insert/upsert (s.CreateOrUpdateDevice or
s.updateDevice) create a TOCTOU race under concurrent load; to fix, move the
count+insert logic into a single database transaction or enforce the limit with
a DB-level constraint: start a transaction, re-check the device count and
perform CreateOrUpdateDevice or updateDevice within that same transaction (or
add a unique/partial/index/constraint in the devices table and handle the
constraint error in CreateOrUpdateDevice), ensuring s.deviceLimit is enforced
atomically.

In `@pkg/services/anonymous/anonimpl/client.go`:
- Around line 44-50: The TagDevice call on anonDeviceService inside client.go is
now synchronous and blocks the authentication path; change it to run
asynchronously (fire-and-forget) so DB writes don't add request latency—invoke
a.anonDeviceService.TagDevice in a background goroutine and capture/log errors
there instead of blocking the response flow; additionally, if you must return
ErrDeviceLimitReached from TagDevice, wrap it in a clearer domain error or
sentinel (e.g., ErrAnonDeviceLimit or a descriptive error from the client
method) so the HTTP handler can map it to a 429/403 with a user-facing message,
and ensure existing log calls (a.log.Warn) remain for diagnostics.
🧹 Nitpick comments (5)
pkg/services/anonymous/anonimpl/anonstore/database.go (2)

72-103: updateDevice conflates "device not found" with "device limit reached".

When rowsAffected == 0, it could mean the device doesn't exist or the updated_at is outside the BETWEEN window. Both cases return ErrDeviceLimitReached. This works in the current call path (only invoked when the limit is hit), but if updateDevice is ever called independently or error messages are surfaced to the user, the ambiguity could be misleading. A comment clarifying this design intent would help.


16-16: Duplicate constant anonymousDeviceExpiration across packages.

The constant is defined identically in both pkg/services/anonymous/anonimpl/anonstore/database.go (line 16) and pkg/services/anonymous/anonimpl/api/api.go (line 18). Export it from one package and reuse it to prevent drift if the value changes.

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

143-148: Error now propagated — non-limit errors will be logged twice.

tagDeviceUI failures are logged at Debug level here (line 146), and then the returned error is logged again at Warn level in client.go (line 49). For ErrDeviceLimitReached this is fine (it short-circuits in client.go), but other tagging errors get dual-logged. Consider removing the Debug log here or only logging when you don't propagate.

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

51-70: Test covers the basic limit-exceeded path; consider adding the "update existing device at limit" case.

The test correctly verifies that a new device is rejected when the limit is reached. However, the other key behavior — that an existing device can still be updated when the limit is hit — is untested. Adding a case where the second CreateOrUpdateDevice call reuses the first device's ID (expecting success) would strengthen confidence in the updateDevice path.

💡 Example additional assertion
// After verifying ErrDeviceLimitReached for a new device,
// updating the existing device should still succeed:
anonDevice.DeviceID = "32mdo31deeqwes" // reuse original ID
anonDevice.UserAgent = "updated-agent"
err = anonDBStore.CreateOrUpdateDevice(context.Background(), anonDevice)
require.NoError(t, err)
packages/grafana-runtime/src/config.ts (1)

97-97: Add an explicit type annotation for type consistency and correctness.

The property anonymousDeviceLimit = undefined; lacks an explicit type annotation. TypeScript infers the type as undefined rather than number | undefined, which doesn't match the GrafanaConfig interface declaration. Other similar optional properties in this class (e.g., loginError: string | undefined = undefined;) include explicit annotations. Adding the type annotation aligns with both the interface contract and the class's established style.

♻️ Proposed fix
-  anonymousDeviceLimit = undefined;
+  anonymousDeviceLimit: number | undefined = undefined;

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)
}
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TOCTOU race: count check and insert are not atomic.

CountDevices and the subsequent CreateOrUpdateDevice upsert execute in separate transactions. Under concurrent load, multiple requests can pass the count check simultaneously and all proceed to insert, overshooting the device limit.

For a soft/approximate limit this may be acceptable, but if strict enforcement is needed, the count + insert should occur within a single DB transaction or use a DB-level constraint.

🤖 Prompt for AI Agents
In `@pkg/services/anonymous/anonimpl/anonstore/database.go` around lines 108 -
118, The CountDevices check in database.go (using s.CountDevices) and the
subsequent insert/upsert (s.CreateOrUpdateDevice or s.updateDevice) create a
TOCTOU race under concurrent load; to fix, move the count+insert logic into a
single database transaction or enforce the limit with a DB-level constraint:
start a transaction, re-check the device count and perform CreateOrUpdateDevice
or updateDevice within that same transaction (or add a
unique/partial/index/constraint in the devices table and handle the constraint
error in CreateOrUpdateDevice), ensuring s.deviceLimit is enforced atomically.

Comment on lines +44 to +50
if err := a.anonDeviceService.TagDevice(ctx, httpReqCopy, anonymous.AnonDeviceUI); err != nil {
if errors.Is(err, anonstore.ErrDeviceLimitReached) {
return nil, err
}
}()

a.log.Warn("Failed to tag anonymous session", "error", err)
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Synchronous DB call now sits on the critical authentication path.

TagDevice was previously asynchronous (per the summary). Making it synchronous means every anonymous request now blocks on a DB write (and potentially a CountDevices query when limits are enabled). Under high anonymous traffic this could add noticeable latency and increase DB load.

Also, returning the raw ErrDeviceLimitReached to the caller will eventually surface as a generic error to the anonymous user. Consider either:

  • wrapping it in a more descriptive error for the HTTP layer, or
  • ensuring the upstream HTTP handler translates it into an appropriate status (e.g., 429 Too Many Requests or 403 Forbidden with a clear message).
🤖 Prompt for AI Agents
In `@pkg/services/anonymous/anonimpl/client.go` around lines 44 - 50, The
TagDevice call on anonDeviceService inside client.go is now synchronous and
blocks the authentication path; change it to run asynchronously
(fire-and-forget) so DB writes don't add request latency—invoke
a.anonDeviceService.TagDevice in a background goroutine and capture/log errors
there instead of blocking the response flow; additionally, if you must return
ErrDeviceLimitReached from TagDevice, wrap it in a clearer domain error or
sentinel (e.g., ErrAnonDeviceLimit or a descriptive error from the client
method) so the HTTP handler can map it to a 429/403 with a user-facing message,
and ensure existing log calls (a.log.Warn) remain for diagnostics.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant