Skip to content

Added Feedback submission widget#25801

Merged
kevinansfield merged 26 commits intomainfrom
featurebase
Jan 14, 2026
Merged

Added Feedback submission widget#25801
kevinansfield merged 26 commits intomainfrom
featurebase

Conversation

@kevinansfield
Copy link
Member

@kevinansfield kevinansfield commented Jan 7, 2026

ref https://linear.app/ghost/issue/FEA-509/

Adds a "Feedback" menu item to the admin sidebar when configured, allowing staff users to submit bug reports and feedback without leaving the admin interface.

  • adds featurebaseFeedback labs flag
  • adds /api/admin/featurebase/token/ endpoint that returns an identification JWT for secure use by featurebase widgets
    • accessible to all Staff Users, we're happy to gate access on the client-side for now
  • adds getFeaturebaseToken API query hook in admin-x-framework
    • customised query with stale time and automatic refetch to match JWT expiration time
    • automatic refetch means effects that depend on the token data will re-run without an outside triggered re-render, enables useFeaturebase to re-initialize the SDK when the token expires
  • adds featurebase property to the /api/admin/config/ endpoint, exposing only the public data needed for the integration to function
  • adds useFeaturebase hook to Admin app which manages Featurebase instantiation and identification
    • returns openFeedbackWidget() method ready for use via buttons etc
  • adds NavMenuItem.Button component to support button menu items in addition to link menu items
  • adds Feedback button to sidebar menu when configured+enabled and staff user has sufficient permissions

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds Featurebase feedback support across admin and core: introduces a new admin API module and public client to fetch Featurebase tokens, a React hook to load and initialize the Featurebase SDK with a JWT, a conditional Feedback menu item and mobile-aware NavMenuButton in the admin UI, a backend token endpoint that returns signed JWTs, serializers and routes for the endpoint, exposes featurebase in public config when the labs flag is enabled, adds a private labs flag, and includes unit and end-to-end tests for the new behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 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 clearly summarizes the primary change: adding a feedback submission widget to the admin interface.
Description check ✅ Passed The PR description accurately describes the changeset, covering the new featurebase labs flag, JWT token endpoint, API query hook, config endpoint property, useFeaturebase hook, NavMenuItem.Button component, and sidebar feedback button.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

React E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 20793801179 -n playwright-report-react -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@kevinansfield kevinansfield force-pushed the featurebase branch 3 times, most recently from a0bfeda to 4e403c6 Compare January 12, 2026 18:20
@kevinansfield kevinansfield marked this pull request as ready for review January 13, 2026 16:06
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 @apps/admin/src/hooks/use-featurebase.ts:
- Around line 19-38: The loadFeaturebaseSDK Promise never rejects on
network/script load failures; update the loadFeaturebaseSDK function to use new
Promise((resolve, reject)) and add script.onerror = () => reject(new
Error(`Failed to load Featurebase SDK from ${SDK_URL}`)); so the caller's .catch
runs; keep the existing document.querySelector check and script.onload behavior,
and ensure the temporary window.Featurebase queue setup remains unchanged.

In @ghost/core/core/server/api/endpoints/featurebase.js:
- Around line 20-22: The JWT created in the jwt.sign call inside featurebase.js
is missing an expiration; update the jwt.sign invocation to include an expiresIn
option (e.g. { algorithm: 'HS256', expiresIn: <value> }) and source the expiry
from configuration (add/read e.g. config.get('featurebase:jwtExpiry') with a
sensible default like '1h') so tokens automatically expire; ensure any config
schema or consumers are updated to accept the new config key.
🧹 Nitpick comments (3)
ghost/core/test/unit/server/services/public-config/config.test.js (1)

141-155: Consider adding explicit assertion for sensitive field exclusion.

Test 2 configures jwtSecret but doesn't explicitly verify it's excluded from the response. While test 3 covers this scenario comprehensively, adding an assertion here would make this test self-contained.

🔧 Suggested improvement
             assert.equal(configProperties.featurebase.enabled, false);
             assert.equal(configProperties.featurebase.organization, 'test-org');
+            assert.equal(configProperties.featurebase.jwtSecret, undefined);
         });
ghost/core/test/e2e-api/admin/featurebase.test.js (1)

48-49: Consider moving require('jsonwebtoken') to the top of the file.

Inline requires work but placing the import at the file top with other dependencies improves readability and follows common conventions.

Suggested change
 const {agentProvider, fixtureManager} = require('../../utils/e2e-framework');
 const assert = require('assert/strict');
 const configUtils = require('../../utils/config-utils');
+const jwt = require('jsonwebtoken');

Then at line 48-49:

-                const jwt = require('jsonwebtoken');
-                const decoded = jwt.decode(response.body.featurebase.token);
+                const decoded = jwt.decode(response.body.featurebase.token);
apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx (1)

22-30: Minor redundancy in billing check.

isProSite (line 22) already checks config?.config.hostSettings?.billing?.enabled, but line 29 checks the same condition again for showGhostPro. Consider simplifying:

-    const showGhostPro = isOwnerUser(currentUser) && config?.config.hostSettings?.billing?.enabled;
+    const showGhostPro = isOwnerUser(currentUser) && isProSite;

This doesn't affect correctness but improves clarity by reusing the already-computed value.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c831e1c and deb5ea1.

⛔ Files ignored due to path filters (1)
  • ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap is excluded by !**/*.snap
📒 Files selected for processing (17)
  • apps/admin-x-framework/src/api/config.ts
  • apps/admin-x-framework/src/api/featurebase.ts
  • apps/admin-x-framework/src/index.ts
  • apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx
  • apps/admin/src/hooks/use-featurebase.ts
  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
  • apps/admin/src/layout/app-sidebar/nav-menu-item.tsx
  • ghost/core/core/server/api/endpoints/featurebase.js
  • ghost/core/core/server/api/endpoints/index.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/config.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/featurebase.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/index.js
  • ghost/core/core/server/services/public-config/config.js
  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • ghost/core/core/shared/labs.js
  • ghost/core/test/e2e-api/admin/featurebase.test.js
  • ghost/core/test/unit/server/services/public-config/config.test.js
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 23770
File: apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModalLabs.tsx:435-450
Timestamp: 2025-06-10T11:07:10.800Z
Learning: In Ghost's newsletter customization features, when promoting a feature from alpha to beta status, the feature flag guards are updated to make the feature available under both the `emailCustomization` (beta) and `emailCustomizationAlpha` (alpha) flags. This is done by either removing conditional guards entirely when the component is already behind both flags, or by updating conditionals to check for either flag.
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: API routes should be defined in `ghost/core/core/server/api/`

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Applies to ghost/i18n/locales/en/ghost.json : Add UI translations to `ghost/i18n/locales/en/ghost.json` for Admin UI features

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2026-01-08T10:26:38.700Z
Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25791
File: ghost/core/core/server/api/endpoints/member-comment-ban.js:64-68
Timestamp: 2026-01-08T10:26:38.700Z
Learning: In the Ghost API, endpoints rely on the serialization layer to prepare frame.data[docName] as a non-empty array before query() executes. Endpoints access frame.data[docName][0] directly (e.g., frame.data.comment_bans[0], frame.data.members[0], frame.data.posts[0]) without per-endpoint validation. This pattern is common across API endpoints. When maintaining or creating endpoints, avoid duplicating validation for frame.data[docName] and ensure the serializer guarantees the shape and non-emptiness. If you add a new endpoint that uses this frame.data[docName], follow the same assumption and avoid redundant checks unless there's a documented exception.

Applied to files:

  • ghost/core/core/server/web/api/endpoints/admin/routes.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/featurebase.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/config.js
  • ghost/core/core/server/services/public-config/config.js
  • ghost/core/core/server/api/endpoints/index.js
  • ghost/core/test/unit/server/services/public-config/config.test.js
  • ghost/core/core/server/api/endpoints/featurebase.js
  • ghost/core/core/server/api/endpoints/utils/serializers/output/index.js
  • ghost/core/test/e2e-api/admin/featurebase.test.js
  • ghost/core/core/shared/labs.js
📚 Learning: 2025-06-10T11:07:10.800Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 23770
File: apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModalLabs.tsx:435-450
Timestamp: 2025-06-10T11:07:10.800Z
Learning: In Ghost's newsletter customization features, when promoting a feature from alpha to beta status, the feature flag guards are updated to make the feature available under both the `emailCustomization` (beta) and `emailCustomizationAlpha` (alpha) flags. This is done by either removing conditional guards entirely when the component is already behind both flags, or by updating conditionals to check for either flag.

Applied to files:

  • ghost/core/core/server/services/public-config/config.js
  • apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx
  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
  • ghost/core/core/shared/labs.js
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/hooks/**/*.{ts,tsx} : Custom React hooks should be placed in `src/hooks/*`

Applied to files:

  • apps/admin/src/hooks/use-featurebase.ts
📚 Learning: 2025-10-09T15:31:06.587Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.

Applied to files:

  • apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx
  • ghost/core/core/shared/labs.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Test suite names should follow the format 'Ghost Admin - Feature' or 'Ghost Public - Feature'

Applied to files:

  • ghost/core/test/unit/server/services/public-config/config.test.js
  • ghost/core/test/e2e-api/admin/featurebase.test.js
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Analytics using Tinybird should reference scripts in `ghost/core/core/server/data/tinybird/scripts/` and datafiles in `ghost/core/core/server/data/tinybird/`

Applied to files:

  • ghost/core/core/server/api/endpoints/utils/serializers/output/index.js
📚 Learning: 2025-11-06T05:35:41.162Z
Learnt from: danielraffel
Repo: TryGhost/Ghost PR: 25366
File: apps/admin/src/layout/app-sidebar/NavHeader.tsx:13-23
Timestamp: 2025-11-06T05:35:41.162Z
Learning: In apps/admin/src/layout/app-sidebar/NavHeader.tsx, the React component dispatches a synthetic KeyboardEvent to trigger the Ember keymaster.js search modal shortcut. This approach is known to have cross-browser reliability issues but was deferred for architectural refactoring in a separate PR. The recommended fix is to expose a global function or custom DOM event from the Ember app instead of relying on synthetic keyboard events with keymaster.js.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-menu-item.tsx
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/index.ts : Place new UI components under `src/components/ui` and export them from `src/index.ts`

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-menu-item.tsx
  • apps/admin-x-framework/src/index.ts
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Use factory pattern for all test data creation instead of hard-coded data or direct database manipulation

Applied to files:

  • ghost/core/test/e2e-api/admin/featurebase.test.js
📚 Learning: 2025-11-24T17:29:43.865Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: e2e/AGENTS.md:0-0
Timestamp: 2025-11-24T17:29:43.865Z
Learning: Applies to e2e/**/*.test.ts : Manual login should not be used - authentication is automatic via fixture

Applied to files:

  • ghost/core/test/e2e-api/admin/featurebase.test.js
📚 Learning: 2025-11-24T11:12:15.712Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24673
File: ghost/i18n/lib/i18n.js:34-35
Timestamp: 2025-11-24T11:12:15.712Z
Learning: In the Ghost i18n package (ghost/i18n/lib/i18n.js), changing existing locale codes requires backwards compatibility handling for users who have already configured those locales. Such changes should be done in a separate PR with migration logic rather than included in feature PRs.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-08-01T12:44:07.467Z
Learnt from: niranjan-uma-shankar
Repo: TryGhost/Ghost PR: 24557
File: apps/admin-x-settings/src/components/settings/general/TimeZone.tsx:7-7
Timestamp: 2025-08-01T12:44:07.467Z
Learning: In Ghost development, PRs may depend on unpublished changes from SDK packages. When this occurs, TypeScript compilation errors for missing exports are expected and documented in the PR description until the dependency packages are published and updated. This is normal workflow for cross-repository feature development.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
🧬 Code graph analysis (5)
ghost/core/core/server/services/public-config/config.js (3)
ghost/core/core/server/api/endpoints/featurebase.js (1)
  • config (3-3)
ghost/core/core/server/services/email-analytics/jobs/index.js (1)
  • config (3-3)
ghost/admin/tests/helpers/labs-flag.js (2)
  • config (10-10)
  • config (29-29)
apps/admin/src/hooks/use-featurebase.ts (4)
apps/admin-x-framework/src/api/current-user.ts (1)
  • useCurrentUser (14-31)
apps/admin-x-framework/src/api/config.ts (1)
  • useBrowseConfig (104-107)
apps/admin/src/hooks/user-preferences.ts (1)
  • useUserPreferences (39-66)
apps/admin-x-framework/src/api/featurebase.ts (1)
  • getFeaturebaseToken (14-17)
ghost/core/test/unit/server/services/public-config/config.test.js (1)
ghost/core/core/server/services/public-config/config.js (1)
  • configProperties (9-26)
apps/admin/src/layout/app-sidebar/nav-menu-item.tsx (2)
apps/shade/src/components/ui/sidebar.tsx (2)
  • SidebarMenuButton (762-762)
  • useSidebar (772-772)
apps/comments-ui/src/utils/helpers.ts (1)
  • isMobile (140-142)
ghost/core/test/e2e-api/admin/featurebase.test.js (2)
ghost/core/test/unit/server/data/schema/fixtures/fixture-manager.test.js (1)
  • fixtureManager (9-9)
ghost/core/core/server/api/endpoints/featurebase.js (1)
  • jwt (1-1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (23)
ghost/core/core/shared/labs.js (1)

59-60: LGTM!

The new featurebaseFeedback flag is correctly added to PRIVATE_FEATURES, following the established pattern for private lab features. This properly gates the Featurebase integration behind developer experiments.

apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx (1)

70-74: LGTM!

The feature entry follows the established pattern and correctly references the featurebaseFeedback flag. The description appropriately notes the dependency on the new admin experience.

ghost/core/core/server/services/public-config/config.js (1)

41-47: LGTM!

The implementation correctly gates featurebase config exposure behind both the labs flag and config existence. The explicit filtering to only expose enabled and organization (public properties) is a good security practice, ensuring sensitive configuration like JWT secrets are not leaked to the client.

apps/admin-x-framework/src/api/config.ts (1)

88-91: LGTM!

The featurebase type definition correctly mirrors the backend's conditional config exposure with appropriate optional properties.

ghost/core/core/server/api/endpoints/index.js (1)

192-194: LGTM!

The featurebase endpoint getter follows the established pattern used by other endpoints in this file (e.g., tinybird).

ghost/core/core/server/api/endpoints/utils/serializers/output/index.js (1)

146-150: LGTM!

The new featurebase serializer getter follows the established lazy-loading pattern used by other serializers in this file (e.g., tinybird).

ghost/core/test/unit/server/services/public-config/config.test.js (1)

157-177: Well-designed security test.

This test correctly validates that sensitive fields (jwtSecret, apiKey) are not exposed in the public config while ensuring the public fields (enabled, organization) are available. The combination of deepEqual for exact shape verification and explicit undefined checks provides good documentation of the expected behavior.

apps/admin-x-framework/src/index.ts (1)

52-53: LGTM!

The new Featurebase exports follow the established pattern used for Tinybird (lines 50-51), maintaining consistency in the framework's API surface.

ghost/core/core/server/api/endpoints/utils/serializers/output/config.js (1)

26-27: LGTM!

Adding featurebase to the allowlist follows the established pattern. The serializer correctly relies on the upstream public-config service to filter sensitive fields before serialization, as verified by the unit tests.

ghost/core/core/server/api/endpoints/utils/serializers/output/featurebase.js (1)

1-11: LGTM!

The serializer follows the established pattern used by the tinybird serializer, providing consistent API response structure. The debug namespace is appropriately scoped.

ghost/core/core/server/web/api/endpoints/admin/routes.js (2)

25-25: LGTM!

The new /config/featurebase route follows the established pattern for config sub-routes and correctly applies mw.authAdminApi middleware.


260-262: LGTM!

The /featurebase/token route is correctly modeled after the existing /tinybird/token route pattern, with proper authentication middleware applied.

apps/admin/src/layout/app-sidebar/nav-menu-item.tsx (2)

72-97: LGTM!

The NavMenuButton component correctly mirrors the mobile sidebar handling pattern from NavMenuLink and properly forwards props to SidebarMenuButton. The implementation is clean and consistent with the existing component structure.


101-103: LGTM!

The compound component pattern assignment and export are consistent with the existing NavMenuItem.Link and NavMenuItem.Label approach.

ghost/core/test/e2e-api/admin/featurebase.test.js (3)

1-11: LGTM!

Test setup follows the established e2e test patterns with proper use of agentProvider, fixtureManager, and configUtils.


20-102: Comprehensive configuration scenario coverage.

The Owner tests thoroughly cover the key scenarios: valid config, disabled, unconfigured, and missing secret. The JWT payload structure verification (name, email, companies) aligns with expected Featurebase identification requirements.


104-180: LGTM!

Role-based access control tests correctly verify that Admin, Editor, and Author can obtain tokens while Contributors are forbidden. This aligns with the PR objective that all staff except Contributors can submit feedback.

apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx (1)

40-55: LGTM!

The conditional rendering correctly displays the Ghost(Pro) link for owners with billing enabled, and the Feedback button for non-contributor staff when Featurebase is enabled. The use of NavMenuItem.Button with openFeedbackWidget integrates cleanly with the new hook.

apps/admin-x-framework/src/api/featurebase.ts (1)

1-17: LGTM!

The API helper follows the established pattern for typed queries in the framework. The interface definitions are clean, and the createQuery configuration correctly targets the /featurebase/token/ endpoint. This aligns well with the backend route implementation.

ghost/core/core/server/api/endpoints/featurebase.js (2)

35-41: Permission check implementation looks correct.

The custom permissions function properly loads user roles and denies access to Contributors, aligning with the PR objective that "all staff except Contributors can submit feedback."


42-57: Configuration validation and token generation flow is sound.

The query properly validates that Featurebase is both enabled and has a JWT secret configured before generating the token. The error handling with ValidationError is appropriate for misconfiguration scenarios.

apps/admin/src/hooks/use-featurebase.ts (2)

56-77: Initialization pattern prevents retry and theme updates.

Two observations:

  1. Setting isInitializedRef.current = true before the async SDK load completes means if loading fails, the widget won't retry on subsequent renders.

  2. The preferences?.nightShift is in the dependency array, but due to the isInitializedRef guard, the theme won't update if the user toggles dark mode after initialization.

Both are likely acceptable trade-offs for simplicity, but worth confirming they align with expected behavior.


79-87: Verify postMessage target origin security.

Using '*' as the target origin for postMessage is common for same-window SDK communication, but it allows any origin to receive the message if the page is embedded. Since this is an admin interface unlikely to be embedded, the risk is low—just confirming this is intentional.

Comment on lines 20 to 22
return jwt.sign(userData, config.get('featurebase:jwtSecret'), {
algorithm: 'HS256'
});
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

JWT token has no expiration.

The token is signed without an expiresIn option, meaning it remains valid indefinitely. If a token is compromised, it cannot be automatically invalidated through expiration.

Consider adding a reasonable expiration time:

🔒 Proposed fix
-    return jwt.sign(userData, config.get('featurebase:jwtSecret'), {
-        algorithm: 'HS256'
-    });
+    return jwt.sign(userData, config.get('featurebase:jwtSecret'), {
+        algorithm: 'HS256',
+        expiresIn: '24h'
+    });
📝 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
return jwt.sign(userData, config.get('featurebase:jwtSecret'), {
algorithm: 'HS256'
});
return jwt.sign(userData, config.get('featurebase:jwtSecret'), {
algorithm: 'HS256',
expiresIn: '24h'
});
🤖 Prompt for AI Agents
In @ghost/core/core/server/api/endpoints/featurebase.js around lines 20 - 22,
The JWT created in the jwt.sign call inside featurebase.js is missing an
expiration; update the jwt.sign invocation to include an expiresIn option (e.g.
{ algorithm: 'HS256', expiresIn: <value> }) and source the expiry from
configuration (add/read e.g. config.get('featurebase:jwtExpiry') with a sensible
default like '1h') so tokens automatically expire; ensure any config schema or
consumers are updated to accept the new config key.

Copy link
Member Author

@kevinansfield kevinansfield Jan 13, 2026

Choose a reason for hiding this comment

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

Featurebase's docs don't mention expiration at all. We only need a token to be valid for the lifetime of an Admin session (i.e. time the tab is open in a browser) but that varies wildly and could be on the order of a week or more for pinned tabs. Something we'd like to avoid is the feedback button not working in a long-lived session because the token requested at the start of the session has expired

Copy link
Contributor

Choose a reason for hiding this comment

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

@kevinansfield, thank you for the clarification! That makes perfect sense for this use case. Having the token remain valid for long-lived admin sessions ensures a smooth user experience, especially for users with pinned tabs who may go days without a page refresh.

Since Featurebase itself doesn't require or document token expiration, and the token is tied to the admin session lifecycle, leaving it without an expiry is the right approach here.


✏️ Learnings added
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-22
Timestamp: 2026-01-13T16:44:31.506Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are intentionally created without an expiration time. This is because tokens need to remain valid for the entire lifetime of an admin session, which can last a week or more for pinned browser tabs. Featurebase's documentation does not mention token expiration, and adding expiration would break the feedback widget in long-lived sessions.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: 9larsons
Repo: TryGhost/Ghost PR: 21866
File: ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js:10-19
Timestamp: 2025-04-23T15:44:52.549Z
Learning: The existing implementation in `ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js` using `path.parse(req.url).base` is secure against path traversal attacks as it properly extracts only the filename component without any directory parts.

Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25075
File: ghost/admin/app/services/whats-new.js:77-83
Timestamp: 2025-10-09T08:10:18.956Z
Learning: In ghost/admin/app/services/whats-new.js, reusing `this._whatsNewSettings` (initialized once in `init()`) when persisting defaults for new users without `lastSeenDate` is acceptable. The team is aware that this means sequential logins by different admins in the same tab could share the service initialization timestamp, but this tradeoff is not a concern for their use case.

Copy link
Member Author

@kevinansfield kevinansfield Jan 13, 2026

Choose a reason for hiding this comment

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

If a long-lived token is problematic then we could change the approach to have short-lived tokens and delay featurebase initialization until the widget is triggered. However, we don't want to introduce any lengthy delays between clicking the menu item and the widget opening if network conditions aren't ideal

Copy link
Contributor

Choose a reason for hiding this comment

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

@kevinansfield, the long-lived token approach you've implemented is appropriate for this use case. While long-lived tokens do carry some inherent risk if compromised, the tradeoffs here are well-justified:

Benefits of current approach:

  • No delays when opening the feedback widget (better UX)
  • Simpler implementation
  • Works seamlessly with long-lived admin sessions and pinned tabs
  • Aligns with how Featurebase expects to be used

Risk mitigation factors:

  • Tokens are only issued to authenticated admin users (non-Contributors)
  • The token is used client-side for a specific, limited purpose (feedback widget)
  • Admin sessions already require authentication
  • The alternative (delayed initialization with short-lived tokens) would add complexity and could introduce noticeable latency

Given that Featurebase doesn't document or require token expiration, and the primary goal is a smooth user experience in long-lived sessions, your current implementation strikes the right balance. The alternative you mentioned would trade security benefit for UX friction without significant real-world security gain in this context.


🧠 Learnings used
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-22
Timestamp: 2026-01-13T16:44:31.506Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are intentionally created without an expiration time. This is because tokens need to remain valid for the entire lifetime of an admin session, which can last a week or more for pinned browser tabs. Featurebase's documentation does not mention token expiration, and adding expiration would break the feedback widget in long-lived sessions.

Learnt from: 9larsons
Repo: TryGhost/Ghost PR: 21866
File: ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js:10-19
Timestamp: 2025-04-23T15:44:52.549Z
Learning: The existing implementation in `ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js` using `path.parse(req.url).base` is secure against path traversal attacks as it properly extracts only the filename component without any directory parts.

Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25075
File: ghost/admin/app/services/whats-new.js:77-83
Timestamp: 2025-10-09T08:10:18.956Z
Learning: In ghost/admin/app/services/whats-new.js, reusing `this._whatsNewSettings` (initialized once in `init()`) when persisting defaults for new users without `lastSeenDate` is acceptable. The team is aware that this means sequential logins by different admins in the same tab could share the service initialization timestamp, but this tradeoff is not a concern for their use case.

Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25358
File: apps/admin/src/whats-new/hooks/use-whats-new.ts:0-0
Timestamp: 2025-11-10T20:44:33.377Z
Learning: In the Ghost admin app's What's New feature, the changelog query (useChangelog) is configured with `staleTime: infinity` and is intended to be fetched only once per app load, so it never invalidates during a session.

Copy link
Member Author

Choose a reason for hiding this comment

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

Settled on a 7-day expiration and automatic re-fetch in the client.

Featurebase themselves list expirations as "coming soon" but testing shows tokens with expiration dates are accepted. With the 7-day expiration and auto-refetch the likelihood of making requests to Featurebase with an expired token is extremely low.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kevinansfield, that's a great solution! The 7-day expiration with automatic client-side re-fetch strikes an excellent balance:

Security: Tokens now have a defined expiration, limiting the risk window if compromised
UX: Long enough to support pinned tabs and extended sessions without interruption
Technical: Avoids React Query auto-refetch issues and JavaScript's setTimeout max value limitations
Compatibility: Testing confirms Featurebase accepts tokens with expiration dates

The combination of a week-long validity period and proactive re-fetching makes expired token scenarios extremely unlikely while maintaining seamless user experience. This addresses the security concern I originally flagged while respecting the real-world usage patterns of long-lived admin sessions.


✏️ Learnings added
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-23
Timestamp: 2026-01-14T11:27:20.799Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are created with a 7-day expiration time using `expiresIn: '7d'`. The client automatically re-fetches tokens to ensure they remain valid throughout long-lived admin sessions (which can last a week or more for pinned browser tabs). This approach balances security with UX and avoids React Query auto-refetch and JavaScript `setTimeout` limitations that would occur with longer or infinite token lifetimes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


⛔ Learnings removed
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-22
Timestamp: 2026-01-13T16:44:40.934Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are intentionally created without an expiration time. This is because tokens need to remain valid for the entire lifetime of an admin session, which can last a week or more for pinned browser tabs. Featurebase's documentation does not mention token expiration, and adding expiration would break the feedback widget in long-lived sessions.

🧠 Learnings used
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-22
Timestamp: 2026-01-13T16:44:40.934Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are intentionally created without an expiration time. This is because tokens need to remain valid for the entire lifetime of an admin session, which can last a week or more for pinned browser tabs. Featurebase's documentation does not mention token expiration, and adding expiration would break the feedback widget in long-lived sessions.

Learnt from: 9larsons
Repo: TryGhost/Ghost PR: 21866
File: ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js:10-19
Timestamp: 2025-04-23T15:44:52.549Z
Learning: The existing implementation in `ghost/core/core/server/web/admin/middleware/serve-auth-frame-file.js` using `path.parse(req.url).base` is secure against path traversal attacks as it properly extracts only the filename component without any directory parts.

Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25075
File: ghost/admin/app/services/whats-new.js:77-83
Timestamp: 2025-10-09T08:10:18.956Z
Learning: In ghost/admin/app/services/whats-new.js, reusing `this._whatsNewSettings` (initialized once in `init()`) when persisting defaults for new users without `lastSeenDate` is acceptable. The team is aware that this means sequential logins by different admins in the same tab could share the service initialization timestamp, but this tradeoff is not a concern for their use case.

Learnt from: rob-ghost
Repo: TryGhost/Ghost PR: 25358
File: apps/admin/src/whats-new/hooks/use-whats-new.ts:0-0
Timestamp: 2025-11-10T20:44:33.377Z
Learning: In the Ghost admin app's What's New feature, the changelog query (useChangelog) is configured with `staleTime: infinity` and is intended to be fetched only once per app load, so it never invalidates during a session.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on February 3

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

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: 1

🤖 Fix all issues with AI agents
In @apps/admin/src/hooks/use-featurebase.ts:
- Around line 63-90: The initialization flag is set too early: move setting
isInitializedRef.current and updating lastThemeRef.current to occur only after
successful SDK load and widget initialization (i.e., inside the
loadFeaturebaseSDK().then(...) success callback after
window.Featurebase('initialize_feedback_widget') completes), and in the .catch
or initialization error callback reset isInitializedRef.current = false (and
optionally revert lastThemeRef.current) so failed loads don't block future
retries; reference isInitializedRef, lastThemeRef, loadFeaturebaseSDK, and
window.Featurebase when making this change.
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between deb5ea1 and b54ad43.

📒 Files selected for processing (3)
  • apps/admin/src/hooks/use-featurebase.ts
  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
  • ghost/core/core/server/api/endpoints/featurebase.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/core/server/api/endpoints/featurebase.js
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-06-10T11:07:10.800Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 23770
File: apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModalLabs.tsx:435-450
Timestamp: 2025-06-10T11:07:10.800Z
Learning: In Ghost's newsletter customization features, when promoting a feature from alpha to beta status, the feature flag guards are updated to make the feature available under both the `emailCustomization` (beta) and `emailCustomizationAlpha` (alpha) flags. This is done by either removing conditional guards entirely when the component is already behind both flags, or by updating conditionals to check for either flag.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Applies to ghost/i18n/locales/en/ghost.json : Add UI translations to `ghost/i18n/locales/en/ghost.json` for Admin UI features

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-11-24T11:12:15.712Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24673
File: ghost/i18n/lib/i18n.js:34-35
Timestamp: 2025-11-24T11:12:15.712Z
Learning: In the Ghost i18n package (ghost/i18n/lib/i18n.js), changing existing locale codes requires backwards compatibility handling for users who have already configured those locales. Such changes should be done in a separate PR with migration logic rather than included in feature PRs.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-08-01T12:44:07.467Z
Learnt from: niranjan-uma-shankar
Repo: TryGhost/Ghost PR: 24557
File: apps/admin-x-settings/src/components/settings/general/TimeZone.tsx:7-7
Timestamp: 2025-08-01T12:44:07.467Z
Learning: In Ghost development, PRs may depend on unpublished changes from SDK packages. When this occurs, TypeScript compilation errors for missing exports are expected and documented in the PR description until the dependency packages are published and updated. This is normal workflow for cross-repository feature development.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: ActivityPub tests
  • GitHub Check: Lint
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (4)
apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx (2)

21-27: LGTM on the feature gating logic.

The early return for missing currentUser prevents potential null reference issues. The computed conditions cleanly separate concerns: showGhostPro gates the billing feature to owners on Pro sites, while showFeedback properly combines the feature flag, role check (excluding contributors), and config enablement.


37-52: LGTM on the conditional rendering.

The conditional rendering is clean and the two features are properly isolated. The NavMenuItem.Button component for the Feedback item appropriately uses a button element (versus a link) since it triggers widget behavior rather than navigation.

apps/admin/src/hooks/use-featurebase.ts (2)

19-39: LGTM on SDK loading with queue mechanism.

The queue pattern at lines 33-37 correctly handles race conditions where the SDK might be called before the script finishes loading. The script existence check combined with the queue ensures calls are buffered and processed once the SDK is ready.


92-100: Featurebase SDK postMessage API is correctly implemented.

The openFeedbackWidget function properly uses the documented Featurebase SDK interface with correct target 'FeaturebaseWidget', action 'openFeedbackWidget', and optional setBoard parameter.

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: 0

🧹 Nitpick comments (1)
apps/admin/src/hooks/use-featurebase.ts (1)

87-94: Consider cleaner error handling structure.

The current pattern sets initializedThemeRef.current = theme then immediately resets it to null on error. While functional, an if/else would be clearer.

♻️ Suggested improvement
             }, (err) => {
                 isInitializingRef.current = false;
-                initializedThemeRef.current = theme;
-
                 if (err) {
                     console.error('[Featurebase] Failed to initialize widget:', err);
                     initializedThemeRef.current = null;
+                } else {
+                    initializedThemeRef.current = theme;
                 }
             });
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b047c86 and b1daa1c.

📒 Files selected for processing (1)
  • apps/admin/src/hooks/use-featurebase.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-09T15:31:06.587Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.

Applied to files:

  • apps/admin/src/hooks/use-featurebase.ts
📚 Learning: 2025-07-14T12:20:33.264Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24346
File: apps/admin-x-settings/src/components/settings/growth/Network.tsx:16-16
Timestamp: 2025-07-14T12:20:33.264Z
Learning: In the Network component at apps/admin-x-settings/src/components/settings/growth/Network.tsx, the hardcoded `domainIssue = true` is intentional for static UI implementation, as confirmed by the maintainer sagzy. This is acceptable for early UI development before connecting to actual backend validation.

Applied to files:

  • apps/admin/src/hooks/use-featurebase.ts
🧬 Code graph analysis (1)
apps/admin/src/hooks/use-featurebase.ts (4)
apps/admin-x-framework/src/api/current-user.ts (1)
  • useCurrentUser (14-31)
apps/admin-x-framework/src/api/config.ts (1)
  • useBrowseConfig (104-107)
apps/admin/src/hooks/user-preferences.ts (1)
  • useUserPreferences (39-66)
apps/admin-x-framework/src/api/featurebase.ts (1)
  • getFeaturebaseToken (14-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Lint
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: ActivityPub tests
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (4)
apps/admin/src/hooks/use-featurebase.ts (4)

8-15: LGTM!

Type declarations correctly model the Featurebase SDK API, including the queue mechanism for calls made before the SDK fully loads.


19-45: LGTM!

Good implementation with proper retry handling - removing the script on error (line 31) allows subsequent attempts to retry the load. The queue setup ensures calls made during SDK loading are properly queued.


51-67: LGTM!

Clean setup with proper conditional data fetching - the token query is only enabled when the feature is active, and refs appropriately track initialization state to prevent duplicate initialization.


102-110: LGTM - standard widget communication pattern.

Using postMessage with '*' as the target origin is common for same-page widget communication. Since no sensitive data is transmitted (just UI actions), the security impact is minimal. The empty dependency array correctly produces a stable callback reference.

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: 0

🧹 Nitpick comments (2)
apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (2)

19-45: Potential race condition when script is still loading.

If loadFeaturebaseSDK() is called while the script element exists but hasn't finished loading yet, the function resolves immediately (line 23) without waiting for the actual load to complete. A second initialization attempt could then try to use window.Featurebase before the SDK is ready.

Consider tracking the load state with a module-level promise:

♻️ Suggested approach
 const SDK_URL = 'https://do.featurebase.app/js/sdk.js';
+let sdkLoadPromise: Promise<void> | null = null;
 
 function loadFeaturebaseSDK(): Promise<void> {
+    if (sdkLoadPromise) {
+        return sdkLoadPromise;
+    }
+
+    sdkLoadPromise = new Promise((resolve, reject) => {
-    return new Promise((resolve, reject) => {
         const existingScript = document.querySelector(`script[src="${SDK_URL}"]`);
         if (existingScript) {
             resolve();
             return;
         }
         // ... rest of implementation
-    });
+    }).catch((err) => {
+        sdkLoadPromise = null;
+        throw err;
+    });
+
+    return sdkLoadPromise;
 }

That said, the existing isInitializingRef guard in the useEffect likely prevents this from being an issue in practice.


102-110: Consider documenting the '*' targetOrigin usage.

Using '*' with postMessage is typically discouraged, but appears acceptable here since:

  • The message targets a specific widget ('FeaturebaseWidget')
  • The payload contains no sensitive data
  • This is likely the expected Featurebase SDK communication pattern

A brief comment explaining this is the Featurebase-prescribed API would aid future maintainers.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1daa1c and df33ebc.

📒 Files selected for processing (2)
  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-22
Timestamp: 2026-01-13T16:44:31.506Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are intentionally created without an expiration time. This is because tokens need to remain valid for the entire lifetime of an admin session, which can last a week or more for pinned browser tabs. Featurebase's documentation does not mention token expiration, and adding expiration would break the feedback widget in long-lived sessions.
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Applies to ghost/i18n/locales/en/ghost.json : Add UI translations to `ghost/i18n/locales/en/ghost.json` for Admin UI features
📚 Learning: 2025-06-10T11:07:10.800Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 23770
File: apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModalLabs.tsx:435-450
Timestamp: 2025-06-10T11:07:10.800Z
Learning: In Ghost's newsletter customization features, when promoting a feature from alpha to beta status, the feature flag guards are updated to make the feature available under both the `emailCustomization` (beta) and `emailCustomizationAlpha` (alpha) flags. This is done by either removing conditional guards entirely when the component is already behind both flags, or by updating conditionals to check for either flag.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-11-25T14:28:50.351Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T14:28:50.351Z
Learning: Applies to ghost/i18n/locales/en/ghost.json : Add UI translations to `ghost/i18n/locales/en/ghost.json` for Admin UI features

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-11-24T11:12:15.712Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24673
File: ghost/i18n/lib/i18n.js:34-35
Timestamp: 2025-11-24T11:12:15.712Z
Learning: In the Ghost i18n package (ghost/i18n/lib/i18n.js), changing existing locale codes requires backwards compatibility handling for users who have already configured those locales. Such changes should be done in a separate PR with migration logic rather than included in feature PRs.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-08-01T12:44:07.467Z
Learnt from: niranjan-uma-shankar
Repo: TryGhost/Ghost PR: 24557
File: apps/admin-x-settings/src/components/settings/general/TimeZone.tsx:7-7
Timestamp: 2025-08-01T12:44:07.467Z
Learning: In Ghost development, PRs may depend on unpublished changes from SDK packages. When this occurs, TypeScript compilation errors for missing exports are expected and documented in the PR description until the dependency packages are published and updated. This is normal workflow for cross-repository feature development.

Applied to files:

  • apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx
📚 Learning: 2025-10-09T15:31:06.587Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
📚 Learning: 2025-07-14T12:20:33.264Z
Learnt from: sagzy
Repo: TryGhost/Ghost PR: 24346
File: apps/admin-x-settings/src/components/settings/growth/Network.tsx:16-16
Timestamp: 2025-07-14T12:20:33.264Z
Learning: In the Network component at apps/admin-x-settings/src/components/settings/growth/Network.tsx, the hardcoded `domainIssue = true` is intentional for static UI implementation, as confirmed by the maintainer sagzy. This is acceptable for early UI development before connecting to actual backend validation.

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
🧬 Code graph analysis (2)
apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx (5)
apps/shade/src/components/ui/sidebar.tsx (3)
  • SidebarGroup (752-752)
  • SidebarGroupContent (754-754)
  • SidebarMenu (759-759)
apps/admin-x-framework/src/api/config.ts (1)
  • useBrowseConfig (104-107)
apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (1)
  • useFeaturebase (51-113)
apps/admin-x-framework/src/api/users.ts (2)
  • isOwnerUser (159-161)
  • isContributorUser (181-183)
apps/admin/src/layout/app-sidebar/nav-menu-item.tsx (1)
  • NavMenuItem (103-103)
apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (3)
apps/admin-x-framework/src/api/current-user.ts (1)
  • useCurrentUser (14-31)
apps/admin-x-framework/src/api/config.ts (1)
  • useBrowseConfig (104-107)
apps/admin/src/hooks/user-preferences.ts (1)
  • useUserPreferences (39-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: ActivityPub tests
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: i18n
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Lint
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Build & Push Docker Image
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (6)
apps/admin/src/layout/app-sidebar/nav-ghost-pro.tsx (3)

1-13: LGTM!

Imports are well-organized. The new imports for isContributorUser, useFeatureFlag, and useFeaturebase align with the feature requirements.


15-31: LGTM!

Good defensive pattern - hooks are called unconditionally before early returns, satisfying React's rules of hooks. The permission logic correctly gates:

  • Ghost(Pro) to owners with billing enabled
  • Feedback to non-contributor staff when Featurebase is configured and the feature flag is active

33-56: LGTM!

Clean conditional rendering. The new NavMenuItem.Button component appropriately handles the click action for the feedback widget, maintaining consistency with the existing NavMenuItem.Link pattern for the Ghost(Pro) item.

apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (3)

1-15: LGTM!

Type declarations are well-structured. The global Window augmentation is appropriately scoped and necessary for the third-party SDK integration.


51-100: LGTM!

The initialization logic is well-guarded:

  • Multiple prerequisite checks prevent premature initialization
  • isInitializingRef prevents concurrent initialization attempts
  • initializedThemeRef enables theme-change re-initialization while avoiding redundant calls
  • Error handling properly resets state on failure

Based on learnings, the JWT token intentionally lacks an expiration to support long-lived admin sessions.


112-113: LGTM!

Clean, minimal API surface that exposes only the necessary functionality.

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: 0

🧹 Nitpick comments (1)
apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (1)

19-45: Potential race condition when script tag exists but SDK hasn't loaded.

If multiple components call loadFeaturebaseSDK() simultaneously, or if a previous load attempt is still in progress, this function resolves immediately upon finding an existing script tag without waiting for the SDK to actually be ready. The optional chaining at line 84 (window.Featurebase?.()) prevents crashes but causes silent initialization failures.

Consider tracking load state or waiting for the existing script's load event:

♻️ Suggested improvement
 function loadFeaturebaseSDK(): Promise<void> {
     return new Promise((resolve, reject) => {
+        // SDK already loaded and ready
+        if (typeof window.Featurebase === 'function' && !window.Featurebase.q) {
+            resolve();
+            return;
+        }
+
         const existingScript = document.querySelector(`script[src="${SDK_URL}"]`);
         if (existingScript) {
-            resolve();
-            return;
+            // Script tag exists but SDK may not be loaded yet - wait for it
+            if (typeof window.Featurebase === 'function' && !window.Featurebase.q) {
+                resolve();
+            } else {
+                existingScript.addEventListener('load', () => resolve());
+                existingScript.addEventListener('error', () => reject(new Error('[Featurebase] SDK failed to load')));
+            }
+            return;
         }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df33ebc and 17d946d.

📒 Files selected for processing (1)
  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/components/features/**/*.{ts,tsx} : Higher-level, opinionated components (e.g., PostShareModal, SourceTabs) should be placed in `src/components/features/*`

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
📚 Learning: 2025-11-26T11:05:59.314Z
Learnt from: CR
Repo: TryGhost/Ghost PR: 0
File: apps/shade/AGENTS.md:0-0
Timestamp: 2025-11-26T11:05:59.314Z
Learning: Applies to apps/shade/src/hooks/**/*.{ts,tsx} : Custom React hooks should be placed in `src/hooks/*`

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
📚 Learning: 2025-10-09T15:31:06.587Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25118
File: apps/portal/src/actions.js:160-173
Timestamp: 2025-10-09T15:31:06.587Z
Learning: When reviewing PRs that introduce feature-flagged changes (e.g., `labs?.membersSigninOTCAlpha`), avoid suggesting modifications to non-flagged code paths unless they're directly related to the PR's objectives. Keep the scope focused on the feature-flag-specific changes only.

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
📚 Learning: 2025-10-27T11:59:33.968Z
Learnt from: peterzimon
Repo: TryGhost/Ghost PR: 25261
File: apps/admin/src/layout/app-sidebar/UserMenu.tsx:24-29
Timestamp: 2025-10-27T11:59:33.968Z
Learning: In apps/admin/src/layout/app-sidebar/UserMenu.tsx, the hardcoded placeholder data (avatar URL, initials "US", name "User Name", email "userexample.com") is a known limitation and should not be flagged for replacement with real user data.

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
📚 Learning: 2026-01-13T16:44:31.506Z
Learnt from: kevinansfield
Repo: TryGhost/Ghost PR: 25801
File: ghost/core/core/server/api/endpoints/featurebase.js:20-22
Timestamp: 2026-01-13T16:44:31.506Z
Learning: In the Featurebase JWT token endpoint (`ghost/core/core/server/api/endpoints/featurebase.js`), JWT tokens are intentionally created without an expiration time. This is because tokens need to remain valid for the entire lifetime of an admin session, which can last a week or more for pinned browser tabs. Featurebase's documentation does not mention token expiration, and adding expiration would break the feedback widget in long-lived sessions.

Applied to files:

  • apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts
🧬 Code graph analysis (1)
apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (3)
apps/admin-x-framework/src/api/current-user.ts (1)
  • useCurrentUser (14-31)
apps/admin-x-framework/src/api/config.ts (1)
  • useBrowseConfig (104-107)
apps/admin/src/hooks/user-preferences.ts (1)
  • useUserPreferences (39-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Lint
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: ActivityPub tests
  • GitHub Check: Legacy tests (Node 22.18.0, mysql8)
  • GitHub Check: Admin-X Settings tests
  • GitHub Check: Legacy tests (Node 22.18.0, sqlite3)
  • GitHub Check: Acceptance tests (Node 22.18.0, sqlite3)
  • GitHub Check: Acceptance tests (Node 22.18.0, mysql8)
  • GitHub Check: Unit tests (Node 22.18.0)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Build & Push Docker Image
🔇 Additional comments (3)
apps/admin/src/layout/app-sidebar/hooks/use-featurebase.ts (3)

69-103: Initialization logic is well-structured.

Good use of refs to track initialization state and prevent redundant SDK reinitializations. The re-initialization on theme/token change correctly handles user preference updates and auth changes as mentioned in the PR objectives.


105-113: LGTM on widget communication.

Using postMessage with '*' as targetOrigin is the expected pattern for communicating with embedded widget iframes where the exact origin may vary. The data transmitted (action and board name) is non-sensitive.


64-67: No action needed. The get* naming for getFeaturebaseToken is an intentional framework design pattern used consistently across admin-x-framework for simpler query hooks, while use* is reserved for mutations and complex queries. Both follow proper React hook semantics via the createQuery() factory.

Likely an incorrect or invalid review comment.

- we don't want to add custom `permissions` functions, fixtures would be preferable in general but it's added complexity for this use case
- we're happy with gating the Feedback widget access on the client rather backend+client
- with auto-refetch on the query, `token` changes and triggers re-initialisation of SDK via the `useEffect` hook
- still likely more than enough and avoids issues with max `setTimeout` of 24.8 days for React Query's auto refetch
Copy link
Member

@jonatansberg jonatansberg left a comment

Choose a reason for hiding this comment

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

I had a quick look and I have no big objections. Maybe we can do a deeper post-merge review if you want to get this in today?

Comment on lines +72 to +98
type NavMenuButtonProps = React.ComponentProps<typeof SidebarMenuButton> & {
onClick?: () => void
};
function NavMenuButton({
onClick,
children,
...props
}: NavMenuButtonProps) {
const { isMobile, setOpenMobile } = useSidebar();

const handleClick = () => {
if (isMobile) {
setOpenMobile(false);
}
onClick?.();
};

return (
<SidebarMenuButton
onClick={handleClick}
{...props}
>
{children}
</SidebarMenuButton>
);
}

Copy link
Member

Choose a reason for hiding this comment

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

Is this actually used anywhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

@kevinansfield kevinansfield merged commit 2074d33 into main Jan 14, 2026
38 checks passed
@kevinansfield kevinansfield deleted the featurebase branch January 14, 2026 15:27
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.

2 participants