Skip to content

Automated Test: implement-device-limits #335

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

  • New Features
    • Added configurable anonymous device limit to control the maximum number of devices tracked per instance.
    • Implemented device limit enforcement that prevents exceeding the limit and returns an error when capacity is reached.
    • Exposed anonymous device limit setting in frontend configuration for instance-wide visibility.

* 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

Configuration field anonymousDeviceLimit is added to TypeScript and Go configuration types, then propagated through frontend settings API. The backend enforces this limit in the anonymous device store by tracking device counts and rejecting new devices when limits are exceeded. Device tagging is converted from async to synchronous with explicit error handling.

Changes

Cohort / File(s) Summary
Configuration Types
packages/grafana-data/src/types/config.ts, packages/grafana-runtime/src/config.ts, pkg/setting/setting.go
Added anonymousDeviceLimit field to TypeScript config interfaces and Go Cfg struct; updated initialization to read from auth.anonymous section.
Frontend Settings API
pkg/api/dtos/frontend_settings.go, pkg/api/frontendsettings.go
Exposed AnonymousDeviceLimit in FrontendSettingsDTO and populated from backend configuration.
Anonymous Device Store
pkg/services/anonymous/anonimpl/anonstore/database.go, pkg/services/anonymous/anonimpl/anonstore/database_test.go
Added anonymousDeviceExpiration constant, ErrDeviceLimitReached error, deviceLimit field to AnonDBStore. Enforces limit in CreateOrUpdateDevice by updating existing devices instead of creating new ones when at capacity. Updated all test call sites to pass deviceLimit parameter.
Anonymous Service Layer
pkg/services/anonymous/anonimpl/api/api.go, pkg/services/anonymous/anonimpl/client.go, pkg/services/anonymous/anonimpl/impl.go, pkg/services/anonymous/anonimpl/impl_test.go
Renamed thirtyDays constant to anonymousDeviceExpiration. Converted device tagging from async goroutine to synchronous call with error propagation for ErrDeviceLimitReached. Modified ProvideAnonymousDeviceService to construct anonStore from sqlStore and pass deviceLimit through initialization chain.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A limit most fair for devices galore,
Anonymous friends can't exceed anymore!
From sync to async, we've turned the tide,
With errors explicit, no more we hide.
Configuration flows through, both front and back,
Devices capped neatly—no overflow slack! 🎯

🚥 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 vague and does not clearly describe the substantive changes in the pull request, which implement device limit enforcement for anonymous devices across multiple components. Replace with a more descriptive title like 'Implement device limits for anonymous devices' or 'Add anonymous device limit enforcement' to better reflect the actual technical changes.
✅ 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/services/anonymous/anonimpl/impl.go (1)

100-115: ⚠️ Potential issue | 🟡 Minor

Bug: Wrong error variable logged.

Line 113 logs err (the function parameter) instead of errD (the delete error). This will log nil since the function returns early on line 103 if err != nil.

🐛 Proposed fix
 	errD := a.anonStore.DeleteDevice(ctx, deviceID)
 	if errD != nil {
-		a.log.Debug("Failed to untag device", "error", err)
+		a.log.Debug("Failed to untag device", "error", errD)
 	}
🤖 Fix all issues with AI agents
In `@packages/grafana-runtime/src/config.ts`:
- Line 97: The property anonymousDeviceLimit is initialized to undefined without
a type, so add an explicit type annotation matching the GrafanaConfig interface
(e.g., anonymousDeviceLimit: number | undefined = undefined) where
anonymousDeviceLimit is declared/initialized to ensure the inferred type allows
merging numeric boot data and stays consistent with GrafanaConfig.

In `@pkg/services/anonymous/anonimpl/anonstore/database.go`:
- Around line 52-54: The wire binding references anonstore.ProvideAnonDBStore
but the constructor now requires a primitive deviceLimit (int64) that wire can't
inject; update the DI binding in pkg/server/wire.go to either remove the bare
reference to ProvideAnonDBStore and use manual initialization in impl.go, or
change the provider set to call ProvideAnonDBStore with a concrete deviceLimit
value (or a provider that returns int64) so the deviceLimit parameter is
satisfied; locate ProvideAnonDBStore and the wire binding entry for anonstore in
wire.go and ensure the provider set matches the new signature.
🧹 Nitpick comments (6)
pkg/services/anonymous/anonimpl/anonstore/database.go (2)

16-18: Export constant to avoid duplication.

The anonymousDeviceExpiration constant is also defined in pkg/services/anonymous/anonimpl/api/api.go. Consider exporting this constant so it can be reused, ensuring consistency if the expiration window changes.

Proposed fix
-const anonymousDeviceExpiration = 30 * 24 * time.Hour
+// AnonymousDeviceExpiration defines the time window for tracking anonymous devices.
+const AnonymousDeviceExpiration = 30 * 24 * time.Hour

Then in api/api.go, import and use anonstore.AnonymousDeviceExpiration.


108-118: Document the soft limit behavior regarding concurrent requests.

The check-then-act pattern between CountDevices and the insert/update is not atomic, allowing concurrent requests to potentially exceed the limit briefly. This is likely acceptable for a soft limit, but adding a code comment would clarify this intentional design decision.

Suggested documentation
 func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) error {
 	var query string

-	// if device limit is reached, only update devices
+	// If device limit is reached, only update existing devices.
+	// Note: This is a soft limit - concurrent requests may briefly exceed the limit
+	// due to the non-atomic check-then-act pattern. This is acceptable as the limit
+	// is enforced on subsequent requests.
 	if s.deviceLimit > 0 {
pkg/services/anonymous/anonimpl/api/api.go (1)

18-18: Consider importing the constant from anonstore.

This constant duplicates anonymousDeviceExpiration defined in anonstore/database.go. If the exported constant suggestion from the database.go review is adopted, this file should import and use anonstore.AnonymousDeviceExpiration to maintain consistency.

Proposed fix (after exporting from anonstore)
-const anonymousDeviceExpiration = 30 * 24 * time.Hour

Then update line 71:

-	fromTime := time.Now().Add(-anonymousDeviceExpiration)
+	fromTime := time.Now().Add(-anonstore.AnonymousDeviceExpiration)
pkg/services/anonymous/anonimpl/anonstore/database_test.go (1)

51-70: Good test coverage for the device limit error path.

The test correctly validates that ErrDeviceLimitReached is returned when attempting to add a new device beyond the limit. However, consider adding a test case that validates the updateDevice fallback path—when a device already exists and the limit is reached, an update to that existing device should succeed (per the CreateOrUpdateDevice logic in database.go).

💡 Suggested additional test case
func TestIntegrationDeviceLimitAllowsUpdates(t *testing.T) {
	store := db.InitTestDB(t)
	anonDBStore := ProvideAnonDBStore(store, 1)

	anonDevice := &Device{
		DeviceID:  "existing-device",
		ClientIP:  "10.30.30.2",
		UserAgent: "test",
		UpdatedAt: time.Now().Add(-time.Hour),
	}

	err := anonDBStore.CreateOrUpdateDevice(context.Background(), anonDevice)
	require.NoError(t, err)

	// Update the same device - should succeed even at limit
	anonDevice.ClientIP = "10.30.30.3"
	anonDevice.UpdatedAt = time.Now()
	err = anonDBStore.CreateOrUpdateDevice(context.Background(), anonDevice)
	require.NoError(t, err)
}
pkg/services/anonymous/anonimpl/client.go (1)

30-59: Synchronous device tagging adds latency to authentication.

The change from async to sync means the Authenticate method now waits for the DB operation in TagDevice to complete. While this enables proper error handling, it may increase authentication latency, especially under high load or slow DB conditions.

If latency becomes a concern, consider whether the device limit check could be optimized (e.g., cached device count) or if non-critical tagging failures should still be handled asynchronously.

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

144-148: Consider differentiating log messages for device limit vs other errors.

The debug log "Failed to tag device for UI" treats all errors the same, but ErrDeviceLimitReached is expected behavior rather than a failure. Additionally, the caller in client.go (line 49) logs a warning for non-limit errors, which could result in the same error being logged at both Debug and Warn levels.

Consider either:

  1. Removing this log since the caller handles logging, or
  2. Differentiating the message for ErrDeviceLimitReached
💡 Option: Remove debug log to avoid double logging
 	err = a.tagDeviceUI(ctx, httpReq, taggedDevice)
 	if err != nil {
-		a.log.Debug("Failed to tag device for UI", "error", err)
 		return err
 	}

@@ -94,6 +94,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
theme2: GrafanaTheme2;
featureToggles: FeatureToggles = {};
anonymousEnabled = false;
anonymousDeviceLimit = undefined;

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add explicit type annotation for type consistency.

The property is initialized to undefined but lacks an explicit type annotation. TypeScript will infer the type as undefined rather than number | undefined, which could cause type errors when the actual numeric value is merged from boot data. This should match the interface definition in GrafanaConfig.

Proposed fix
-  anonymousDeviceLimit = undefined;
+  anonymousDeviceLimit: number | undefined = undefined;
📝 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
anonymousDeviceLimit = undefined;
anonymousDeviceLimit: number | undefined = undefined;
🤖 Prompt for AI Agents
In `@packages/grafana-runtime/src/config.ts` at line 97, The property
anonymousDeviceLimit is initialized to undefined without a type, so add an
explicit type annotation matching the GrafanaConfig interface (e.g.,
anonymousDeviceLimit: number | undefined = undefined) where anonymousDeviceLimit
is declared/initialized to ensure the inferred type allows merging numeric boot
data and stays consistent with GrafanaConfig.

Comment on lines +52 to 54
func ProvideAnonDBStore(sqlStore db.DB, deviceLimit int64) *AnonDBStore {
return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore"), deviceLimit: deviceLimit}
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if wire.go still references ProvideAnonDBStore and how it's used

# Find wire bindings for ProvideAnonDBStore
echo "=== Wire bindings for ProvideAnonDBStore ==="
rg -n "ProvideAnonDBStore" --type go

# Check if there's any direct usage in wire sets
echo -e "\n=== Full context of wire usage ==="
rg -n -B5 -A5 "anonstore\.ProvideAnonDBStore|ProvideAnonDBStore" --type go -g '*wire*'

Repository: admin-coderabbit/coderabbit-grafana-mirror

Length of output: 1633


Remove or update wire binding for new constructor signature.

The wire binding at pkg/server/wire.go:375 references anonstore.ProvideAnonDBStore as a bare function pointer without providing the now-required deviceLimit parameter. Since deviceLimit is a primitive type (int64), wire cannot auto-inject it. This will cause a compilation error. Either remove the wire binding if manual initialization in impl.go is sufficient, or update it to provide the deviceLimit parameter.

🤖 Prompt for AI Agents
In `@pkg/services/anonymous/anonimpl/anonstore/database.go` around lines 52 - 54,
The wire binding references anonstore.ProvideAnonDBStore but the constructor now
requires a primitive deviceLimit (int64) that wire can't inject; update the DI
binding in pkg/server/wire.go to either remove the bare reference to
ProvideAnonDBStore and use manual initialization in impl.go, or change the
provider set to call ProvideAnonDBStore with a concrete deviceLimit value (or a
provider that returns int64) so the deviceLimit parameter is satisfied; locate
ProvideAnonDBStore and the wire binding entry for anonstore in wire.go and
ensure the provider set matches the new signature.

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