Skip to content

Conversation

@riderx
Copy link
Member

@riderx riderx commented Feb 10, 2026

Summary (AI generated)

  • Stop /stats from querying apps a second time for allow_device_custom_id; reuse the existing getAppOwnerPostgres lookup.
  • Extend the app-status cache payload to include allow_device_custom_id for the cancelled fast-path.
  • When plan is cancelled, do not emit customIdBlocked (still drop custom_id if disabled).

Test plan (AI generated)

  • bun lint:backend
  • bun typecheck
  • bun lint

Screenshots (AI generated)

  • N/A (backend-only)

Checklist (AI generated)

  • My code follows the code style of this project and passes bun run lint:backend && bun run lint.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce my tests

Generated with AI

Summary by CodeRabbit

  • New Features

    • Enhanced device custom ID feature flag management with improved caching and payload propagation.
  • Bug Fixes

    • Improved device custom ID handling for cancelled subscription plans with optimized status synchronization.
  • Tests

    • Added tests validating device custom ID behavior for cancelled plans and cached app status scenarios.

Copilot AI review requested due to automatic review settings February 10, 2026 15:54
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 optimizes the /stats plugin endpoint by removing a redundant apps table lookup for allow_device_custom_id, instead reusing the existing getAppOwnerPostgres data and extending the app-status cache payload to carry this flag for the cancelled fast-path.

Changes:

  • Extend the app status cache payload to optionally include allow_device_custom_id and add getAppStatusPayload() to read it.
  • Remove the extra apps query in /stats by using appOwner.allow_device_custom_id (and cached payload on the cancelled fast-path).
  • Update callers that set cached app status (update, stats, channel_self) to include allow_device_custom_id in the cached payload.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
supabase/functions/_backend/utils/update.ts Include allow_device_custom_id when caching cloud/cancelled app status so downstream fast-paths can reuse it.
supabase/functions/_backend/utils/appStatus.ts Introduce AppStatusPayload, add getAppStatusPayload(), and allow setAppStatus() to store optional metadata in cache.
supabase/functions/_backend/plugins/stats.ts Stop querying apps solely for allow_device_custom_id; use cached payload for cancelled fast-path and appOwner elsewhere.
supabase/functions/_backend/plugins/channel_self.ts Include allow_device_custom_id when caching cloud/cancelled status for consistency with the new cache payload shape.

Comment on lines 88 to 96
if (cachedStatus === 'cancelled') {
const statsActions: StatsActions[] = [{ action: 'needPlanUpgrade' }]
// Keep behavior backward compatible (default allow=true), but allow owners to
// disable custom_id persistence from unauthenticated /stats traffic.
if (!allowDeviceCustomId && device.custom_id !== undefined) {
const allowDeviceCustomId = hasCustomId ? (cached?.allow_device_custom_id ?? true) : true
if (!allowDeviceCustomId && hasCustomId) {
device.custom_id = undefined
statsActions.push({ action: 'customIdBlocked' })
}
await sendStatsAndDevice(c, device, statsActions)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The new cancelled fast-path changes behavior (drop device.custom_id when cached allow_device_custom_id is false, and no longer emits the customIdBlocked stat). There are existing tests around customIdBlocked emission, but none that cover the cancelled-cache path; please add/adjust tests to cover this specific behavior so regressions are caught (including verifying no customIdBlocked entry is written when status is cancelled).

Copilot uses AI. Check for mistakes.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6c6323f82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to +93
const allowDeviceCustomId = hasCustomId ? (cached?.allow_device_custom_id ?? true) : true
if (!allowDeviceCustomId && hasCustomId) {

Choose a reason for hiding this comment

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

P2 Badge Avoid stale custom_id policy on cancelled cache hits

In the cancelled fast-path, the custom ID gate now depends only on cached metadata (cached?.allow_device_custom_id ?? true) and never re-reads the app row, so a cancelled app that changes allow_device_custom_id can keep accepting/persisting custom_id until the app-status cache expires. This regresses the immediate-enforcement behavior /stats previously had for custom ID requests (it opened a primary DB connection for these requests), and during mixed-version rollout it also treats status-only cache entries as allowed by default.

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

The pull request refactors app status management to propagate metadata (specifically allow_device_custom_id) through a new AppStatusPayload interface. It replaces direct database queries with cached payload retrieval, adds a special-case override where cancelled apps without Stripe are treated as 'cloud' in the cache, and implements fast-path caching for cancelled/on-prem statuses to avoid hot-path latency.

Changes

Cohort / File(s) Summary
Core App Status Utilities
supabase/functions/_backend/utils/appStatus.ts
Introduced AppStatusPayload interface with optional allow_device_custom_id field; added getAppStatusPayload() to fetch cached payload with special-case override (cancelled + no Stripe → status lifted to 'cloud'); refactored setAppStatus() signature to accept optional payload and use eager write for cancelled/on-prem vs. background task for cloud.
Plugin Integration
supabase/functions/_backend/plugins/channel_self.ts, supabase/functions/_backend/plugins/stats.ts, supabase/functions/_backend/utils/update.ts
Updated setAppStatus() calls to pass optional payload object { allow_device_custom_id: ... }; replaced database-driven allowDeviceCustomIdFromPg with cached getAppStatusPayload() derivation; removed direct PG reads and apply allow_device_custom_id locally via cache or appOwner values.
Public API Cache Sync
supabase/functions/_backend/public/app/put.ts
Added best-effort cache synchronization: when body.allow_device_custom_id is updated, fetch cached status and update it if status is 'cancelled' to enforce immediately.
Test Coverage
tests/stats.test.ts
Added STRIPE_CONFIGURED flag and conditional test execution; introduced two new tests for cancelled plan scenarios: one verifying custom_id is dropped and needPlanUpgrade is recorded; another validating fast-path behavior via cache with deleted app row.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant stats.ts as Plugin (stats.ts)
    participant appStatus as Utility (appStatus.ts)
    participant Cache
    participant DB

    Note over Client,DB: Old Flow: Direct DB Query
    Client->>stats.ts: POST /stats
    stats.ts->>DB: allowDeviceCustomIdFromPg()
    DB->>stats.ts: allow_device_custom_id value
    stats.ts->>stats.ts: Apply allow_device_custom_id check
    stats.ts-->>Client: 200/429 Response

    Note over Client,DB: New Flow: Cached Payload with Override
    Client->>stats.ts: POST /stats
    stats.ts->>appStatus: getAppStatusPayload(appId)
    appStatus->>Cache: Fetch cached payload
    alt Cache Hit
        Cache->>appStatus: Return { status, allow_device_custom_id }
        Note over appStatus: Special case:<br/>status='cancelled' + no Stripe<br/>→ lift to 'cloud'
    else Cache Miss
        appStatus->>DB: Query appOwner.allow_device_custom_id
        DB->>appStatus: Value or null
        appStatus->>Cache: Write payload
    end
    appStatus->>stats.ts: AppStatusPayload
    stats.ts->>stats.ts: Derive allow_device_custom_id<br/>from cache or appOwner
    stats.ts->>appStatus: setAppStatus(c, appId, status, { allow_device_custom_id })
    appStatus->>Cache: Write combined payload
    stats.ts-->>Client: 200/429 Response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A cache comes to life, swift and true,
Where statuses dance with metadata too,
No more database queries in the fast lane—
Cancelled apps leap to the cloud domain!
Payloads propagate, custom IDs aligned, 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(stats): avoid extra apps query' directly describes the main change: eliminating a redundant database query in the stats endpoint by reusing existing lookups.
Description check ✅ Passed The pull request description follows the template structure with Summary, Test plan, Screenshots, and Checklist sections, all appropriately filled for a backend-only change.

✏️ 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 riderx/stats-app-flag

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.

@riderx riderx force-pushed the riderx/stats-app-flag branch from 2dafe08 to 71bd735 Compare February 11, 2026 03:31
Copy link
Contributor

@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

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/utils/update.ts`:
- Line 88: getAppOwnerPostgres is not selecting or returning the
apps.allow_device_custom_id field but update.ts calls
appOwner.allow_device_custom_id when calling setAppStatus; update
getAppOwnerPostgres to include allow_device_custom_id in the SELECT (and in its
TypeScript return type/interface) so the returned object has
allow_device_custom_id available for callers like update.ts and matches
setAppStatus's expected payload.

In `@tests/stats.test.ts`:
- Around line 14-16: The regex used to set STRIPE_CONFIGURED in
tests/stats.test.ts uses a capturing group; change it to a non-capturing group
so the linter warning goes away by replacing the pattern used when computing the
STRIPE_CONFIGURED constant (currently /(sk_|rk_)/ inside /^(...)/) with a
non-capturing group (?:sk_|rk_) so the line that defines STRIPE_CONFIGURED
continues to test the same prefixes but without creating an unused capture.
🧹 Nitpick comments (1)
supabase/functions/_backend/utils/appStatus.ts (1)

25-37: Dead-code guard: buildAppStatusRequest always returns a truthy object.

buildAppStatusRequest (line 17-23) unconditionally returns { helper, request }, so the if (!cacheEntry) check on line 27 can never be true. Not harmful, but misleading.

}
if (!appOwner.plan_valid) {
await setAppStatus(c, app_id, 'cancelled')
await setAppStatus(c, app_id, 'cancelled', { allow_device_custom_id: appOwner.allow_device_custom_id })
Copy link
Contributor

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

# Find pg.ts file
find . -name "pg.ts" -path "*/supabase/functions/_backend/*"

Repository: Cap-go/capgo

Length of output: 97


🏁 Script executed:

#!/bin/bash

# Search for getAppOwnerPostgres function definition
rg -n "getAppOwnerPostgres" --type=ts -B2 -A15 -g "*/pg.ts"

Repository: Cap-go/capgo

Length of output: 204


🏁 Script executed:

#!/bin/bash

# Check update.ts lines around 88 and 99
rg -n "setAppStatus" --type=ts -B2 -A2 supabase/functions/_backend/utils/update.ts | head -30

Repository: Cap-go/capgo

Length of output: 1209


🏁 Script executed:

#!/bin/bash

# Check for the apps table schema to see if allow_device_custom_id exists
find . -name "*schema*" -o -name "*migrations*" | grep -E "(postgre|schema|migration)" | head -10

Repository: Cap-go/capgo

Length of output: 564


🏁 Script executed:

#!/bin/bash

# Read pg.ts file and look for getAppOwnerPostgres
cat ./supabase/functions/_backend/utils/pg.ts | head -300

Repository: Cap-go/capgo

Length of output: 11764


🏁 Script executed:

#!/bin/bash

# Search for getAppOwnerPostgres more carefully
grep -n "getAppOwnerPostgres" ./supabase/functions/_backend/utils/pg.ts -A 30

Repository: Cap-go/capgo

Length of output: 2452


🏁 Script executed:

#!/bin/bash

# Check postgres_schema.ts for the apps table definition
cat ./supabase/functions/_backend/utils/postgres_schema.ts | grep -A 50 "apps"

Repository: Cap-go/capgo

Length of output: 7806


🏁 Script executed:

#!/bin/bash

# Check for setAppStatus function in appStatus.ts to understand what parameters it accepts
find . -name "appStatus.ts" -path "*/supabase/functions/_backend/*" -exec cat {} \;

Repository: Cap-go/capgo

Length of output: 2242


Fix getAppOwnerPostgres to include allow_device_custom_id in the return type.

The TypeScript build fails because getAppOwnerPostgres in supabase/functions/_backend/utils/pg.ts does not select or return allow_device_custom_id, yet update.ts (lines 88, 99) attempts to access this property from the returned object. The field exists in the apps table schema and setAppStatus accepts it in the payload; it just needs to be included in the query and return type of getAppOwnerPostgres.

🧰 Tools
🪛 GitHub Actions: Run tests

[error] 88-88: Property 'allow_device_custom_id' does not exist on type '{ owner_org: string; orgs: { created_by: string; id: string; management_email: string; }; plan_valid: boolean; channel_device_count: number; manifest_bundle_count: number; expose_metadata: boolean; }'.

🤖 Prompt for AI Agents
In `@supabase/functions/_backend/utils/update.ts` at line 88, getAppOwnerPostgres
is not selecting or returning the apps.allow_device_custom_id field but
update.ts calls appOwner.allow_device_custom_id when calling setAppStatus;
update getAppOwnerPostgres to include allow_device_custom_id in the SELECT (and
in its TypeScript return type/interface) so the returned object has
allow_device_custom_id available for callers like update.ts and matches
setAppStatus's expected payload.

Comment on lines +14 to +16
// Cancelled app-status cache is only honored when Stripe is configured in the worker env.
// CI/local Cloudflare tests typically don't configure Stripe, so we skip cache-specific tests there.
const STRIPE_CONFIGURED = /^(sk_|rk_)/.test(env.STRIPE_SECRET_KEY || '')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

ESLint: use a non-capturing group in the regex.

Static analysis flags the unused capturing group. Since you only need to test membership, switch to (?:...).

🔧 Proposed fix
-const STRIPE_CONFIGURED = /^(sk_|rk_)/.test(env.STRIPE_SECRET_KEY || '')
+const STRIPE_CONFIGURED = /^(?:sk_|rk_)/.test(env.STRIPE_SECRET_KEY || '')
📝 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
// Cancelled app-status cache is only honored when Stripe is configured in the worker env.
// CI/local Cloudflare tests typically don't configure Stripe, so we skip cache-specific tests there.
const STRIPE_CONFIGURED = /^(sk_|rk_)/.test(env.STRIPE_SECRET_KEY || '')
// Cancelled app-status cache is only honored when Stripe is configured in the worker env.
// CI/local Cloudflare tests typically don't configure Stripe, so we skip cache-specific tests there.
const STRIPE_CONFIGURED = /^(?:sk_|rk_)/.test(env.STRIPE_SECRET_KEY || '')
🧰 Tools
🪛 ESLint

[error] 16-16: Capturing group number 1 is defined but never used.

(regexp/no-unused-capturing-group)

🤖 Prompt for AI Agents
In `@tests/stats.test.ts` around lines 14 - 16, The regex used to set
STRIPE_CONFIGURED in tests/stats.test.ts uses a capturing group; change it to a
non-capturing group so the linter warning goes away by replacing the pattern
used when computing the STRIPE_CONFIGURED constant (currently /(sk_|rk_)/ inside
/^(...)/) with a non-capturing group (?:sk_|rk_) so the line that defines
STRIPE_CONFIGURED continues to test the same prefixes but without creating an
unused capture.

@sonarqubecloud
Copy link

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.

1 participant